import { describe, expect, it } from "vitest"; import { contrastRatio, contrastReport, formatColour, gamutReport, interpolateColourStops, interpolateStops, mapToGamut, nearestPassingColour, relativeLuminance, } from "../../src/colour"; describe("multi-stop interpolation", () => { it("interpolates across positioned stops", () => { const stops = [ { colour: "#ff0000", position: 0 }, { colour: "#00ff00", position: 0.5 }, { colour: "#0000ff", position: 1 }, ]; expect( formatColour( interpolateColourStops(stops, 0.25, { space: "srgb" }), "hex", ), ).toBe("#808000"); expect( formatColour( interpolateColourStops(stops, 0.75, { space: "srgb" }), "hex", ), ).toBe("#008080"); }); it("distributes omitted positions and includes exact endpoints", () => { const steps = interpolateStops( [{ colour: "black" }, { colour: "red" }, { colour: "white" }], 5, { space: "srgb", }, ); expect(steps.map((step) => step.hex)).toEqual([ "#000000", "#800000", "#ff0000", "#ff8080", "#ffffff", ]); expect(steps.map((step) => step.position)).toEqual([0, 0.25, 0.5, 0.75, 1]); }); it("takes the shorter hue path across zero", () => { const middle = interpolateColourStops( [{ colour: "hsl(350 100% 50%)" }, { colour: "hsl(10 100% 50%)" }], 0.5, { space: "hsl", hue: "shorter" }, ); expect(formatColour(middle, "hex")).toBe("#ff0000"); }); it("supports deterministic easing", () => { const linear = interpolateColourStops( [{ colour: "black" }, { colour: "white" }], 0.5, { space: "srgb" }, ); const eased = interpolateColourStops( [{ colour: "black" }, { colour: "white" }], 0.5, { space: "srgb", easing: "ease-in", }, ); expect(linear.coords[0]).toBeCloseTo(0.5); expect(eased.coords[0]).toBeCloseTo(0.25); }); it("premultiplies alpha by default to avoid transparent colour fringes", () => { const premultiplied = interpolateColourStops( [{ colour: "rgba(255, 0, 0, 0)" }, { colour: "rgba(0, 0, 255, 1)" }], 0.5, { space: "srgb" }, ); const straight = interpolateColourStops( [{ colour: "rgba(255, 0, 0, 0)" }, { colour: "rgba(0, 0, 255, 1)" }], 0.5, { space: "srgb", premultiplied: false }, ); expect(premultiplied.alpha).toBeCloseTo(0.5); expect(premultiplied.coords).toEqual([0, 0, 1]); expect(straight.coords).toEqual([0.5, 0, 0.5]); }); }); describe("gamut and accessibility", () => { it("reports and maps wide-gamut colours", () => { const report = gamutReport("color(display-p3 1 0 0)"); expect(report.spaces.find((space) => space.space === "srgb")?.inGamut).toBe( false, ); expect(report.spaces.find((space) => space.space === "p3")?.inGamut).toBe( true, ); expect( report.spaces.find((space) => space.space === "p3")?.mappedCss, ).toMatch(/^color\(display-p3 /); const mapped = mapToGamut("color(display-p3 1 0 0)"); expect( mapped.coords.every((channel) => channel >= -1e-9 && channel <= 1 + 1e-9), ).toBe(true); }); it("uses WCAG relative luminance and contrast thresholds", () => { expect(relativeLuminance("black")).toBe(0); expect(relativeLuminance("white")).toBe(1); expect(contrastRatio("black", "white")).toBe(21); const report = contrastReport("#777", "white"); expect(report.ratio).toBeCloseTo(4.478089, 5); expect(report.passes.aaNormal).toBe(false); expect(report.passes.aaLarge).toBe(true); }); it("flattens alpha against the visible background", () => { expect(contrastRatio("rgba(0,0,0,.5)", "white")).toBeCloseTo(3.976653, 5); expect( contrastReport("black", "rgba(255,255,255,.5)", { canvas: "black" }) .ratio, ).toBeCloseTo(5.280823, 4); }); it("finds a nearby foreground that passes a requested target", () => { const suggestion = nearestPassingColour("#777", "white", 4.5); expect(suggestion).not.toBeNull(); expect(suggestion?.direction).toBe("darker"); expect(suggestion?.ratio).toBeGreaterThanOrEqual(4.5); expect(suggestion?.deltaEOK).toBeLessThan(0.02); }); });