/** * Parity tests for the canonical style comparison engine. * * These tests are adapted from packages/viewer/src/__tests__/style-utils.test.ts * to prove that the core engine produces identical results to the previous * viewer and CLI local implementations. */ import { describe, it, expect } from "vitest"; import { compareStyles, compareStyleValue, normalizeStyleValue, compareColors, parseColor, compareNumericValues, compareStylesWithTokens, formatTokenSummary, getComplianceBadge, DEFAULT_STYLE_PROPERTIES, DEFAULT_ENHANCED_STYLE_PROPERTIES, } from "../style-comparison.js"; import type { TokenLookup, StyleDiffItem, StyleComparisonResult, EnhancedStyleComparisonResult, NormalizedToken, NormalizedStyleMap, StyleComparisonOptions, } from "../style-comparison.js"; // ─── parseColor ────────────────────────────────────────────────────────────── describe("parseColor", () => { describe("hex colors", () => { it("parses 6-digit hex color", () => { expect(parseColor("#ff0000")).toEqual({ r: 255, g: 0, b: 0 }); }); it("parses lowercase hex color", () => { expect(parseColor("#00ff00")).toEqual({ r: 0, g: 255, b: 0 }); }); it("parses mixed case hex color", () => { expect(parseColor("#0000FF")).toEqual({ r: 0, g: 0, b: 255 }); }); it("parses hex color with values 0-255", () => { expect(parseColor("#336699")).toEqual({ r: 51, g: 102, b: 153 }); }); it("returns null for 3-digit hex color", () => { expect(parseColor("#f00")).toBeNull(); }); }); describe("rgb/rgba colors", () => { it("parses rgb color", () => { expect(parseColor("rgb(255, 0, 0)")).toEqual({ r: 255, g: 0, b: 0, a: 1, }); }); it("parses rgba color with alpha", () => { expect(parseColor("rgba(255, 0, 0, 0.5)")).toEqual({ r: 255, g: 0, b: 0, a: 0.5, }); }); it("parses rgba color with alpha 0", () => { expect(parseColor("rgba(0, 0, 0, 0)")).toEqual({ r: 0, g: 0, b: 0, a: 0, }); }); it("parses rgba color with alpha 1", () => { expect(parseColor("rgba(100, 150, 200, 1)")).toEqual({ r: 100, g: 150, b: 200, a: 1, }); }); it("handles extra whitespace", () => { expect(parseColor("rgb( 255 , 128 , 64 )")).toEqual({ r: 255, g: 128, b: 64, a: 1, }); }); }); describe("invalid colors", () => { it("returns null for color keywords", () => { expect(parseColor("red")).toBeNull(); expect(parseColor("transparent")).toBeNull(); }); it("returns null for hsl colors", () => { expect(parseColor("hsl(0, 100%, 50%)")).toBeNull(); }); it("returns null for empty string", () => { expect(parseColor("")).toBeNull(); }); }); }); // ─── compareColors ─────────────────────────────────────────────────────────── describe("compareColors", () => { it("matches identical hex colors", () => { expect(compareColors("#ff0000", "#ff0000", 0)).toBe(true); }); it("matches colors within tolerance", () => { expect(compareColors("#ff0000", "#fe0000", 5)).toBe(true); expect(compareColors("rgb(255, 0, 0)", "rgb(252, 0, 0)", 5)).toBe(true); }); it("fails colors outside tolerance", () => { expect(compareColors("#ff0000", "#f00000", 5)).toBe(false); expect(compareColors("rgb(255, 0, 0)", "rgb(240, 0, 0)", 5)).toBe(false); }); it("matches hex and rgba representations of same color", () => { expect(compareColors("#ff0000", "rgba(255, 0, 0, 1)", 0)).toBe(true); }); it("matches colors with similar alpha", () => { expect( compareColors("rgba(255, 0, 0, 0.5)", "rgba(255, 0, 0, 0.52)", 5) ).toBe(true); }); it("fails colors with different alpha", () => { expect( compareColors("rgba(255, 0, 0, 0.5)", "rgba(255, 0, 0, 0.7)", 5) ).toBe(false); }); it("falls back to string comparison for unparseable colors", () => { expect(compareColors("red", "red", 0)).toBe(true); expect(compareColors("red", "blue", 0)).toBe(false); }); }); // ─── compareNumericValues ──────────────────────────────────────────────────── describe("compareNumericValues", () => { it("matches identical values", () => { expect(compareNumericValues("10px", "10px", 0)).toBe(true); }); it("matches values within tolerance", () => { expect(compareNumericValues("10px", "11px", 1)).toBe(true); expect(compareNumericValues("10px", "9px", 1)).toBe(true); }); it("fails values outside tolerance", () => { expect(compareNumericValues("10px", "12px", 1)).toBe(false); }); it("handles values without units", () => { expect(compareNumericValues("10", "11", 1)).toBe(true); }); it("handles decimal values", () => { expect(compareNumericValues("10.5px", "11px", 1)).toBe(true); expect(compareNumericValues("10.5px", "12px", 1)).toBe(false); }); it("falls back to string comparison for non-numeric values", () => { expect(compareNumericValues("auto", "auto", 0)).toBe(true); expect(compareNumericValues("auto", "none", 0)).toBe(false); }); }); // ─── normalizeStyleValue ───────────────────────────────────────────────────── describe("normalizeStyleValue", () => { it("trims whitespace", () => { expect(normalizeStyleValue("any", " 10px ")).toBe("10px"); }); it("collapses multiple spaces", () => { expect(normalizeStyleValue("any", "10px 20px")).toBe("10px 20px"); }); it('normalizes "none" boxShadow to empty string', () => { expect(normalizeStyleValue("boxShadow", "none")).toBe(""); }); it('does not normalize "none" for other properties', () => { expect(normalizeStyleValue("display", "none")).toBe("none"); }); it("normalizes transparent color to 'transparent'", () => { expect(normalizeStyleValue("backgroundColor", "rgba(0, 0, 0, 0)")).toBe( "transparent" ); expect(normalizeStyleValue("borderColor", "rgba( 0 , 0 , 0 , 0 )")).toBe( "transparent" ); }); }); // ─── compareStyleValue ─────────────────────────────────────────────────────── describe("compareStyleValue", () => { describe("exact match", () => { it("matches identical values", () => { expect(compareStyleValue("fontFamily", "Inter", "Inter")).toBe(true); }); it("matches after normalization", () => { expect(compareStyleValue("fontFamily", " Inter ", "Inter")).toBe(true); }); }); describe("color properties", () => { it("uses color comparison for backgroundColor", () => { expect( compareStyleValue("backgroundColor", "#ff0000", "rgb(255, 0, 0)") ).toBe(true); }); it("uses color comparison for borderColor", () => { expect( compareStyleValue("borderColor", "#00ff00", "rgb(0, 255, 0)") ).toBe(true); }); it("allows tolerance in color comparison", () => { expect( compareStyleValue("backgroundColor", "#ff0000", "rgb(252, 0, 0)") ).toBe(true); }); }); describe("numeric properties", () => { it("uses numeric comparison for borderWidth", () => { expect(compareStyleValue("borderWidth", "1px", "2px")).toBe(true); expect(compareStyleValue("borderWidth", "1px", "3px")).toBe(false); }); it("uses numeric comparison for borderRadius", () => { expect(compareStyleValue("borderRadius", "8px", "9px")).toBe(true); }); it("uses numeric comparison for fontSize", () => { expect(compareStyleValue("fontSize", "16px", "17px")).toBe(true); }); it("uses numeric comparison for padding", () => { expect(compareStyleValue("padding", "10px", "11px")).toBe(true); }); it("uses numeric comparison for gap", () => { expect(compareStyleValue("gap", "16px", "17px")).toBe(true); }); }); describe("other properties", () => { it("uses strict comparison for fontFamily", () => { expect(compareStyleValue("fontFamily", "Inter", "Arial")).toBe(false); }); it("uses strict comparison for fontWeight", () => { expect(compareStyleValue("fontWeight", "400", "500")).toBe(false); }); it("uses strict comparison for textAlign", () => { expect(compareStyleValue("textAlign", "left", "center")).toBe(false); }); }); }); // ─── compareStyles ─────────────────────────────────────────────────────────── describe("compareStyles", () => { it("returns match true when all styles match", () => { const figmaStyles = { backgroundColor: "#ff0000", fontSize: "16px", }; const renderedStyles = { backgroundColor: "rgb(255, 0, 0)", fontSize: "16px", }; const result = compareStyles(figmaStyles, renderedStyles); expect(result.match).toBe(true); expect(result.properties).toHaveLength(2); expect(result.properties.every((p) => p.match)).toBe(true); }); it("returns match false when some styles differ", () => { const figmaStyles = { backgroundColor: "#ff0000", fontSize: "16px", }; const renderedStyles = { backgroundColor: "rgb(0, 255, 0)", fontSize: "16px", }; const result = compareStyles(figmaStyles, renderedStyles); expect(result.match).toBe(false); expect( result.properties.find((p) => p.property === "backgroundColor")?.match ).toBe(false); expect( result.properties.find((p) => p.property === "fontSize")?.match ).toBe(true); }); it("only compares properties present in figmaStyles", () => { const figmaStyles = { backgroundColor: "#ff0000", }; const renderedStyles = { backgroundColor: "rgb(255, 0, 0)", fontSize: "16px", fontFamily: "Inter", }; const result = compareStyles(figmaStyles, renderedStyles); expect(result.properties).toHaveLength(1); expect(result.properties[0].property).toBe("backgroundColor"); }); it("marks missing rendered styles as not matching", () => { const figmaStyles = { backgroundColor: "#ff0000", borderRadius: "8px", }; const renderedStyles = { backgroundColor: "rgb(255, 0, 0)", }; const result = compareStyles(figmaStyles, renderedStyles); const borderRadius = result.properties.find( (p) => p.property === "borderRadius" ); expect(borderRadius?.rendered).toBe("(not set)"); expect(borderRadius?.match).toBe(false); }); it("returns cleaned figmaStyles object", () => { const figmaStyles = { backgroundColor: "#ff0000", fontSize: undefined, fontFamily: "Inter", }; const renderedStyles = { backgroundColor: "rgb(255, 0, 0)", fontFamily: "Inter", }; const result = compareStyles(figmaStyles, renderedStyles); expect(result.figmaStyles).toEqual({ backgroundColor: "#ff0000", fontFamily: "Inter", }); expect(result.figmaStyles.fontSize).toBeUndefined(); }); it("preserves renderedStyles in output", () => { const figmaStyles = { backgroundColor: "#ff0000", }; const renderedStyles = { backgroundColor: "rgb(255, 0, 0)", fontSize: "16px", }; const result = compareStyles(figmaStyles, renderedStyles); expect(result.renderedStyles).toBe(renderedStyles); }); it("handles empty figmaStyles", () => { const figmaStyles = {}; const renderedStyles = { backgroundColor: "rgb(255, 0, 0)", }; const result = compareStyles(figmaStyles, renderedStyles); expect(result.match).toBe(true); expect(result.properties).toHaveLength(0); }); it("compares all standard properties when present", () => { const figmaStyles = { backgroundColor: "#ff0000", borderColor: "#000000", borderWidth: "1px", borderRadius: "4px", fontFamily: "Inter", fontSize: "16px", fontWeight: "400", lineHeight: "24px", letterSpacing: "-0.5px", textAlign: "left", boxShadow: "0px 4px 8px rgba(0, 0, 0, 0.25)", padding: "16px", gap: "8px", opacity: "0.9", }; const renderedStyles = { backgroundColor: "rgb(255, 0, 0)", borderColor: "rgb(0, 0, 0)", borderWidth: "1px", borderRadius: "4px", fontFamily: "Inter", fontSize: "16px", fontWeight: "400", lineHeight: "24px", letterSpacing: "-0.5px", textAlign: "left", boxShadow: "0px 4px 8px rgba(0, 0, 0, 0.25)", padding: "16px", gap: "8px", opacity: "0.9", }; const result = compareStyles(figmaStyles, renderedStyles); expect(result.match).toBe(true); expect(result.properties).toHaveLength(14); }); }); // ─── compareStylesWithTokens ───────────────────────────────────────────────── describe("compareStylesWithTokens", () => { it("works without token lookup (basic comparison)", () => { const result = compareStylesWithTokens( { backgroundColor: "#ff0000" }, { backgroundColor: "rgb(255, 0, 0)" } ); expect(result.match).toBe(true); expect(result.properties[0].isHardcoded).toBe(false); expect(result.tokenSummary).toBeUndefined(); }); it("includes color property in enhanced comparison", () => { const result = compareStylesWithTokens( { color: "#000000" }, { color: "#000000" } ); expect(result.properties).toHaveLength(1); expect(result.properties[0].property).toBe("color"); }); it("identifies hardcoded values with token lookup", () => { const mockLookup: TokenLookup = { findByValue: (value: string) => { if (value === "#ff0000") return ["--color-danger"]; return []; }, getToken: (name: string) => { if (name === "--color-danger") { return { name: "--color-danger", rawValue: "#ff0000", resolvedValue: "#ff0000", category: "color", level: 2, referenceChain: [], sourceFile: "tokens.css", theme: "default", selector: ":root", }; } return undefined; }, calculateUsageSummary: () => ({ totalProperties: 1, usingTokens: 0, hardcoded: 1, implicitMatches: 0, compliancePercent: 0, hardcodedProperties: [], }), }; const result = compareStylesWithTokens( { backgroundColor: "#ff0000" }, { backgroundColor: "rgb(255, 0, 0)" }, mockLookup ); expect(result.properties[0].figmaToken).toBe("--color-danger"); expect(result.properties[0].isHardcoded).toBe(true); expect(result.properties[0].suggestedFix).toBeDefined(); expect(result.properties[0].suggestedFix?.tokenName).toBe( "--color-danger" ); expect(result.properties[0].suggestedFix?.codeFix).toBe( "background-color: var(--color-danger);" ); expect(result.tokenSummary).toBeDefined(); }); it("does not flag as hardcoded when rendered uses a token", () => { const mockLookup: TokenLookup = { findByValue: () => ["--color-danger"], getToken: () => undefined, calculateUsageSummary: () => ({ totalProperties: 1, usingTokens: 1, hardcoded: 0, implicitMatches: 0, compliancePercent: 100, hardcodedProperties: [], }), }; const result = compareStylesWithTokens( { backgroundColor: "#ff0000" }, { backgroundColor: "#ff0000" }, mockLookup ); expect(result.properties[0].isHardcoded).toBe(false); }); }); // ─── formatTokenSummary ────────────────────────────────────────────────────── describe("formatTokenSummary", () => { it("formats basic summary", () => { const summary = formatTokenSummary({ totalProperties: 10, usingTokens: 8, hardcoded: 2, implicitMatches: 0, compliancePercent: 80, hardcodedProperties: [], }); expect(summary).toContain("Token Compliance: 80%"); expect(summary).toContain("8/10 properties using tokens"); expect(summary).toContain("2 hardcoded value(s) detected"); }); it("includes implicit matches when present", () => { const summary = formatTokenSummary({ totalProperties: 10, usingTokens: 7, hardcoded: 0, implicitMatches: 3, compliancePercent: 70, hardcodedProperties: [], }); expect(summary).toContain("3 implicit match(es)"); }); it("omits hardcoded line when zero", () => { const summary = formatTokenSummary({ totalProperties: 5, usingTokens: 5, hardcoded: 0, implicitMatches: 0, compliancePercent: 100, hardcodedProperties: [], }); expect(summary).not.toContain("hardcoded"); }); }); // ─── getComplianceBadge ────────────────────────────────────────────────────── describe("getComplianceBadge", () => { it("returns Excellent for 100%", () => { expect(getComplianceBadge(100)).toEqual({ label: "Excellent", color: "green", }); }); it("returns Good for 80-99%", () => { expect(getComplianceBadge(80)).toEqual({ label: "Good", color: "blue" }); expect(getComplianceBadge(99)).toEqual({ label: "Good", color: "blue" }); }); it("returns Fair for 50-79%", () => { expect(getComplianceBadge(50)).toEqual({ label: "Fair", color: "yellow" }); expect(getComplianceBadge(79)).toEqual({ label: "Fair", color: "yellow" }); }); it("returns Poor for below 50%", () => { expect(getComplianceBadge(0)).toEqual({ label: "Poor", color: "red" }); expect(getComplianceBadge(49)).toEqual({ label: "Poor", color: "red" }); }); }); // ─── Constants ─────────────────────────────────────────────────────────────── describe("constants", () => { it("DEFAULT_STYLE_PROPERTIES has 14 entries (no color)", () => { expect(DEFAULT_STYLE_PROPERTIES).toHaveLength(14); expect(DEFAULT_STYLE_PROPERTIES).not.toContain("color"); }); it("DEFAULT_ENHANCED_STYLE_PROPERTIES has 15 entries (includes color)", () => { expect(DEFAULT_ENHANCED_STYLE_PROPERTIES).toHaveLength(15); expect(DEFAULT_ENHANCED_STYLE_PROPERTIES).toContain("color"); }); it("enhanced properties is a superset of standard properties", () => { for (const prop of DEFAULT_STYLE_PROPERTIES) { expect(DEFAULT_ENHANCED_STYLE_PROPERTIES).toContain(prop); } }); }); // ─── Contract types compile-time check ─────────────────────────────────────── describe("contract types", () => { it("NormalizedToken shape is valid", () => { const token: NormalizedToken = { name: "--color-primary", value: "#0051c2", category: "color", source: "css-var", originalName: "--color-primary", theme: "default", }; expect(token.name).toBe("--color-primary"); expect(token.source).toBe("css-var"); }); it("NormalizedStyleMap is a string record", () => { const map: NormalizedStyleMap = { backgroundColor: "#ff0000", fontSize: "16px", }; expect(map.backgroundColor).toBe("#ff0000"); }); it("StyleComparisonOptions has expected shape", () => { const opts: StyleComparisonOptions = { colorTolerance: 10, numericTolerance: 2, alphaTolerance: 0.1, properties: ["backgroundColor"], theme: "dark", }; expect(opts.colorTolerance).toBe(10); }); });