Files
colour-tools/tests/colour/interpolate-accessibility.test.ts
T

143 lines
4.2 KiB
TypeScript

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);
});
});