import fc from "fast-check"; import { describe, expect, it } from "vitest"; import { conversionRows, formatColour, parseColour, parseColourList, toSrgbPreview, tryParseColour, } from "../../src/colour"; describe("colour parsing and conversion", () => { it("converts the RapidTables reference colour", () => { const colour = parseColour("#f8f5ee"); expect(colour.space).toBe("srgb"); expect(colour.coords).toEqual([248 / 255, 245 / 255, 238 / 255]); expect(formatColour(colour, "hex")).toBe("#f8f5ee"); expect(formatColour(colour, "rgb")).toBe("rgb(248, 245, 238)"); expect(formatColour(colour, "hsl")).toBe("hsl(42 41.6667% 95.2941%)"); }); it("accepts CSS Color 4, named, bare hex and numeric RGB input", () => { expect(formatColour("rebeccapurple", "hex")).toBe("#663399"); expect(formatColour("f8f5ee", "hex")).toBe("#f8f5ee"); expect(formatColour("0xff000080", "hex8")).toBe("#ff000080"); expect(formatColour("248, 245, 238", "hex")).toBe("#f8f5ee"); const p3 = parseColour("color(display-p3 1 0.2 0.1 / 75%)"); expect(p3.space).toBe("p3"); expect(p3.alpha).toBeCloseTo(0.75); }); it("parses custom HSV/HSB with angles and alpha", () => { expect(formatColour("hsv(120 50% 60% / 25%)", "rgba")).toBe( "rgba(77, 153, 77, 0.25)", ); expect(formatColour("hsb(.333333turn, .5, .6)", "hex")).toBe("#4d994d"); expect(parseColour("hsv(-30deg 100% 100%)").coords[0]).toBe(330); }); it("parses custom CMYK and converts it deterministically", () => { expect(formatColour("cmyk(0% 100% 100% 0% / .5)", "hex8")).toBe( "#ff000080", ); expect(formatColour("cmyk(100%, 0%, 100%, 25%)", "rgb")).toBe( "rgb(0, 191, 0)", ); expect(formatColour("#000", "cmyk")).toBe("cmyk(0% 0% 0% 100%)"); }); it("returns structured failures without throwing", () => { expect(tryParseColour("")).toMatchObject({ ok: false, error: { code: "empty" }, }); expect(tryParseColour("hsv(20 200% 20%)")).toMatchObject({ ok: false, error: { code: "out-of-range" }, }); expect(tryParseColour("definitely-not-a-colour").ok).toBe(false); }); it("splits lists only at top-level delimiters", () => { const colours = parseColourList("rgb(1, 2, 3), hsl(20 30% 40%);\n#fff"); expect(colours).toHaveLength(3); expect(formatColour(colours[0]!, "rgb")).toBe("rgb(1, 2, 3)"); expect(parseColourList('["#f00", "#0f0"]')).toHaveLength(2); }); it("offers copy-ready conversion rows and a safe preview", () => { const rows = conversionRows("#f8f5ee", ["hex", "rgb", "oklch"]); expect(rows.map((row) => row.label)).toEqual(["HEX", "RGB", "OKLCH"]); expect(rows[0]?.copyValue).toBe("#f8f5ee"); const preview = toSrgbPreview("color(display-p3 1 0 0)"); expect(preview.wasMapped).toBe(true); expect(preview.css).toMatch(/^rgb\(/); expect(preview.rgb.every((channel) => channel >= 0 && channel <= 1)).toBe( true, ); }); it("round-trips arbitrary 8-bit RGB colours through HEX", () => { fc.assert( fc.property( fc.integer({ min: 0, max: 255 }), fc.integer({ min: 0, max: 255 }), fc.integer({ min: 0, max: 255 }), (red, green, blue) => { const value = { space: "srgb", coords: [red / 255, green / 255, blue / 255] as [ number, number, number, ], alpha: 1, }; expect(parseColour(formatColour(value, "hex")).coords).toEqual( value.coords, ); }, ), { numRuns: 200 }, ); }); });