@@ -71,6 +71,14 @@ test("converts, composites and interpolates without transient empty results", as
|
||||
await expect(page.locator(".step-swatch")).toHaveCount(9);
|
||||
await page.getByLabel(/Swatches/u).fill("5");
|
||||
await expect(page.locator(".step-swatch")).toHaveCount(5);
|
||||
await page.getByLabel("Gradient type").selectOption("radial");
|
||||
await expect(page.locator(".gradient-preview + pre")).toContainText(
|
||||
"radial-gradient(ellipse at center",
|
||||
);
|
||||
await page.getByText("Repeating", { exact: true }).click();
|
||||
await expect(page.locator(".gradient-preview + pre")).toContainText(
|
||||
"repeating-radial-gradient",
|
||||
);
|
||||
|
||||
expect(externalRequests).toEqual([]);
|
||||
expect(runtimeErrors).toEqual([]);
|
||||
@@ -123,9 +131,35 @@ test("keeps picking, accessibility analysis and palette data browser-local", asy
|
||||
"--brand-primary",
|
||||
);
|
||||
|
||||
await page.getByLabel("Palette data").fill(
|
||||
JSON.stringify({
|
||||
vivid: {
|
||||
$type: "color",
|
||||
$value: {
|
||||
colorSpace: "display-p3",
|
||||
components: [0.9, 0.2, 0.1],
|
||||
},
|
||||
},
|
||||
vividAlias: { $value: "{vivid}" },
|
||||
}),
|
||||
);
|
||||
await page.getByRole("button", { name: "Import colours" }).click();
|
||||
await expect(page.getByRole("status")).toContainText(
|
||||
"Added 1 colour; ignored 1 exact duplicate",
|
||||
);
|
||||
await expect(page.locator(".saved-colour-list > li")).toHaveCount(4);
|
||||
await expect(page.locator(".contrast-matrix tbody tr")).toHaveCount(4);
|
||||
await expect(
|
||||
page.locator(".contrast-matrix tbody tr").first().locator("td"),
|
||||
).toHaveCount(4);
|
||||
await page.getByLabel("Format").selectOption("tokens");
|
||||
await expect(page.locator(".export-preview")).toContainText(
|
||||
"https://www.designtokens.org/schemas/2025.10/format.json",
|
||||
);
|
||||
|
||||
await page.reload();
|
||||
await page.getByRole("tab", { name: /Palette/u }).click();
|
||||
await expect(page.locator(".saved-colour-list > li")).toHaveCount(3);
|
||||
await expect(page.locator(".saved-colour-list > li")).toHaveCount(4);
|
||||
|
||||
expect(externalRequests).toEqual([]);
|
||||
expect(runtimeErrors).toEqual([]);
|
||||
@@ -194,7 +228,7 @@ test("serves a relocatable production artifact with hardened headers", async ({
|
||||
expect(manifest.headers()["content-type"]).toContain("application/json");
|
||||
await expect(manifest.json()).resolves.toMatchObject({
|
||||
id: "de.add-ideas.colour-tools",
|
||||
version: "0.1.0",
|
||||
version: "0.2.0",
|
||||
entry: "./",
|
||||
icon: "./favicon.svg",
|
||||
});
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
|
||||
test("keeps the primary workspace inside a narrow viewport", async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.goto("/deep/nested/colour/");
|
||||
await expect(page.locator("main").first()).toBeVisible();
|
||||
await expect(
|
||||
page.locator("main .loading, main .workbench-loading"),
|
||||
).toHaveCount(0);
|
||||
|
||||
const widths = await page.evaluate(() => ({
|
||||
content: document.documentElement.scrollWidth,
|
||||
viewport: document.documentElement.clientWidth,
|
||||
}));
|
||||
expect(widths.viewport).toBeLessThanOrEqual(430);
|
||||
expect(widths.content).toBeLessThanOrEqual(widths.viewport + 1);
|
||||
});
|
||||
@@ -2,11 +2,15 @@ import { describe, expect, it } from "vitest";
|
||||
|
||||
import {
|
||||
colourHarmony,
|
||||
buildCssGradient,
|
||||
deltaE,
|
||||
exportGradientRecipe,
|
||||
exportPalette,
|
||||
formatColour,
|
||||
normaliseTokenName,
|
||||
parsePaletteList,
|
||||
parseDtcgTokens,
|
||||
paletteContrastMatrix,
|
||||
shades,
|
||||
simulateColourVision,
|
||||
simulateColourVisionSet,
|
||||
@@ -99,13 +103,170 @@ describe("palette construction and export", () => {
|
||||
paper: "#f8f5ee",
|
||||
ink: "#111111",
|
||||
});
|
||||
expect(JSON.parse(exportPalette(entries, "tokens")).paper).toEqual({
|
||||
$type: "color",
|
||||
$value: "#f8f5ee",
|
||||
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(/1–64/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 }],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user