Files
colour-tools/tests/colour/compare-palette.test.ts
T
zemion ed030cada6
Verify / verify (push) Canceled after 0s
Release Colour Tools 0.2.0
2026-09-02 07:11:11 +02:00

273 lines
8.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { describe, expect, it } from "vitest";
import {
colourHarmony,
buildCssGradient,
deltaE,
exportGradientRecipe,
exportPalette,
formatColour,
normaliseTokenName,
parsePaletteList,
parseDtcgTokens,
paletteContrastMatrix,
shades,
simulateColourVision,
simulateColourVisionSet,
tints,
tones,
} from "../../src/colour";
describe("comparison and colour-vision tools", () => {
it("provides standard Delta-E methods", () => {
expect(deltaE("#f8f5ee", "#f8f5ee", "2000")).toBe(0);
expect(deltaE("red", "blue", "76")).toBeGreaterThan(100);
expect(deltaE("#777", "#787878", "ok")).toBeGreaterThan(0);
});
it("simulates four deficiencies while preserving alpha", () => {
const original = "rgba(200, 40, 20, .4)";
const unchanged = simulateColourVision(original, "deuteranopia", {
severity: 0,
});
expect(formatColour(unchanged, "hex8")).toBe("#c8281466");
const simulated = simulateColourVision(original, "deuteranopia");
expect(simulated.alpha).toBeCloseTo(0.4);
expect(formatColour(simulated, "hex")).not.toBe("#c82814");
expect(Object.keys(simulateColourVisionSet(original))).toEqual([
"protanopia",
"deuteranopia",
"tritanopia",
"achromatopsia",
]);
});
it("turns achromatopsia output neutral", () => {
const simulated = simulateColourVision("#ff8000", "achromatopsia");
expect(simulated.coords[0]).toBeCloseTo(simulated.coords[1]!, 8);
expect(simulated.coords[1]).toBeCloseTo(simulated.coords[2]!, 8);
});
});
describe("palette construction and export", () => {
it("builds useful hue harmonies", () => {
expect(colourHarmony("oklch(60% .15 20)", "complementary")).toHaveLength(2);
expect(colourHarmony("oklch(60% .15 20)", "analogous")).toHaveLength(3);
expect(colourHarmony("oklch(60% .15 20)", "square")).toHaveLength(4);
});
it("builds deterministic tint, shade and tone scales", () => {
expect(tints("#336699", 3)).toHaveLength(3);
expect(shades("#336699", 3, { includeBase: true })).toHaveLength(4);
expect(tones("#336699", 2, { includeEndpoint: true })).toHaveLength(2);
expect(
formatColour(tints("black", 1, { includeEndpoint: true })[0]!, "hex"),
).toBe("#ffffff");
});
it("parses named text, CSS variables, arrays and token JSON", () => {
const text = parsePaletteList("Primary: #f8f5ee\n--accent: rgb(20 40 60)");
expect(text.map((entry) => entry.name)).toEqual(["Primary", "accent"]);
expect(parsePaletteList('["red", "blue"]')).toHaveLength(2);
const tokens = parsePaletteList(
'{"brand":{"$type":"color","$value":"#123456"}}',
);
expect(formatColour(tokens[0]!.colour, "hex")).toBe("#123456");
const nested = parsePaletteList(
'{"brand":{"light":{"$type":"color","$value":"#eee"},"dark":{"$value":"#111"}}}',
);
expect(nested.map((entry) => entry.name)).toEqual([
"brand-light",
"brand-dark",
]);
expect(
parsePaletteList(":root { --paper: #f8f5ee; --ink: #111; }"),
).toHaveLength(2);
});
it("normalises and de-duplicates portable token names", () => {
expect(normaliseTokenName(" Crème brûlée / 100 ")).toBe(
"creme-brulee-100",
);
const entries = parsePaletteList("Brand: red\nBrand: blue");
expect(exportPalette(entries, "css", { prefix: "App" })).toContain(
"--app-brand-2: #0000ff;",
);
});
it("exports CSS, Sass, JSON, DTCG, Tailwind and CSV", () => {
const entries = parsePaletteList("paper: #f8f5ee\nink: #111");
expect(exportPalette(entries, "css")).toContain("--colour-paper: #f8f5ee;");
expect(exportPalette(entries, "scss")).toContain("$colour-ink: #111111;");
expect(JSON.parse(exportPalette(entries, "json"))).toEqual({
paper: "#f8f5ee",
ink: "#111111",
});
expect(JSON.parse(exportPalette(entries, "tokens"))).toMatchObject({
$schema: "https://www.designtokens.org/schemas/2025.10/format.json",
paper: {
$type: "color",
$value: {
colorSpace: "srgb",
components: expect.any(Array),
hex: "#f8f5ee",
},
},
});
expect(exportPalette(entries, "tailwind")).toMatch(/^export default /);
expect(exportPalette(entries, "csv")).toBe(
"name,value\npaper,#f8f5ee\nink,#111111",
);
});
it("imports DTCG 2025.10 structured colours and local aliases", () => {
const result = parseDtcgTokens(
JSON.stringify({
$schema: "https://www.designtokens.org/schemas/2025.10/format.json",
base: {
$type: "color",
blue: {
$value: {
colorSpace: "display-p3",
components: [0.2, 0.4, 0.8],
alpha: 0.75,
hex: "#3366cc",
},
},
},
semantic: {
primary: { $type: "color", $value: "{base.blue}" },
},
}),
);
expect(result.entries.map((entry) => entry.name)).toEqual([
"base-blue",
"semantic-primary",
]);
expect(result.diagnostics).toEqual([]);
expect(formatColour(result.entries[0]!.colour, "hex8")).toMatch(
/^#[0-9a-f]{8}$/u,
);
});
it("reports missing DTCG types instead of guessing them", () => {
const result = parseDtcgTokens(
'{"untyped":{"$value":{"colorSpace":"srgb","components":[1,0,0]}}}',
);
expect(result.entries).toEqual([]);
expect(result.diagnostics[0]).toMatchObject({ severity: "error" });
});
it("inherits a DTCG alias type and rejects out-of-range components", () => {
const alias = parseDtcgTokens(
JSON.stringify({
base: {
$type: "color",
$value: { colorSpace: "srgb", components: [1, 0, 0] },
},
alias: { $value: "{base}" },
}),
);
expect(alias.entries.map((entry) => entry.name)).toEqual(["base", "alias"]);
expect(alias.diagnostics).toEqual([]);
const invalid = parseDtcgTokens(
JSON.stringify({
bad: {
$type: "color",
$value: { colorSpace: "display-p3", components: [1.2, 0, 0] },
},
}),
);
expect(invalid.entries).toEqual([]);
expect(invalid.diagnostics[0]?.message).toMatch(/from 0 to 1/u);
});
it("resolves DTCG JSON Pointer aliases and property-level references", () => {
const result = parseDtcgTokens(
JSON.stringify({
base: {
$type: "color",
$value: {
colorSpace: "srgb",
components: [0.2, 0.4, 0.8],
hex: "#3366cc",
},
},
alias: { $ref: "#/base/$value" },
mixed: {
$type: "color",
$value: {
colorSpace: "srgb",
components: [
{ $ref: "#/base/$value/components/0" },
{ $ref: "#/base/$value/components/1" },
0.7,
],
},
},
}),
);
expect(result.entries.map((entry) => entry.name)).toEqual([
"base",
"alias",
"mixed",
]);
expect(result.diagnostics).toEqual([]);
});
it("reports cyclic and malformed DTCG property references", () => {
const result = parseDtcgTokens(
JSON.stringify({
cycle: {
$type: "color",
$value: { $ref: "#/cycle/$value" },
},
siblings: {
$type: "color",
$value: { $ref: "#/cycle/$value", extra: true },
},
}),
);
expect(result.entries).toEqual([]);
expect(result.diagnostics.map((item) => item.message).join(" ")).toMatch(
/cycle|sibling/iu,
);
});
});
describe("palette accessibility and gradients", () => {
it("builds a directional contrast matrix with WCAG thresholds", () => {
const matrix = paletteContrastMatrix(["black", "white", "#777"]);
expect(matrix.size).toBe(3);
expect(matrix.cells[0]?.[1]).toMatchObject({
ratio: 21,
aaNormal: true,
aaaNormal: true,
});
expect(matrix.cells[2]?.[1]?.aaNormal).toBe(false);
expect(() => paletteContrastMatrix([])).toThrow(/164/u);
});
it("exports linear, radial, conic and repeating CSS gradients", () => {
const stops = [
{ colour: "red", position: 0 },
{ colour: "blue", position: 1 },
];
expect(buildCssGradient(stops, { angle: 45 })).toMatch(
/^linear-gradient\(45deg,/u,
);
expect(
buildCssGradient(stops, { kind: "radial", radialShape: "circle" }),
).toMatch(/^radial-gradient\(circle at center,/u);
expect(buildCssGradient(stops, { kind: "conic", repeating: true })).toMatch(
/^repeating-conic-gradient\(/u,
);
expect(JSON.parse(exportGradientRecipe(stops))).toMatchObject({
schemaVersion: 1,
type: "css-gradient",
stops: [{ position: 0 }, { position: 1 }],
});
});
});