143 lines
4.2 KiB
TypeScript
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);
|
|
});
|
|
});
|