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

236 lines
8.3 KiB
TypeScript

import { expect, test, type Page } from "@playwright/test";
const APP_ORIGIN = "http://127.0.0.1:4173";
const FOUR_COLOUR_PNG =
"iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAYAAABytg0kAAAAE0lEQVR4nGP4z8DwHwwZGP6DAQBJyAn3FGMynQAAAABJRU5ErkJggg==";
async function keepNetworkLocal(page: Page): Promise<string[]> {
const externalRequests: string[] = [];
await page.route("**/*", async (route) => {
const requestUrl = new URL(route.request().url());
if (requestUrl.origin !== APP_ORIGIN) {
externalRequests.push(requestUrl.href);
await route.abort();
return;
}
await route.continue();
});
return externalRequests;
}
function recordRuntimeErrors(page: Page): string[] {
const runtimeErrors: string[] = [];
page.on("pageerror", (error) => runtimeErrors.push(error.message));
page.on("console", (message) => {
if (message.type() === "error") runtimeErrors.push(message.text());
});
return runtimeErrors;
}
test("converts, composites and interpolates without transient empty results", async ({
page,
}) => {
const runtimeErrors = recordRuntimeErrors(page);
const externalRequests = await keepNetworkLocal(page);
await page.goto("/deep/nested/colour/");
await expect(
page.getByRole("heading", { name: "Colour workbench" }),
).toBeVisible();
await expect(
page.getByRole("heading", { name: "Convert colours" }),
).toBeVisible();
await page.getByLabel("Colour", { exact: true }).fill("rgb(255 0 0 / 50%)");
const alphaHex = page
.locator(".conversion-row")
.filter({ hasText: "HEX + alpha" });
await expect(alphaHex).toContainText("#ff000080");
await page.getByLabel("Colour", { exact: true }).fill("rgb(255 0");
await expect(
page.getByText(
"Keeping the last valid conversion while you finish editing.",
),
).toBeVisible();
await expect(alphaHex).toContainText("#ff000080");
await page.getByRole("tab", { name: /Composite/u }).click();
await expect(
page.getByRole("heading", { name: "Composite translucent colours" }),
).toBeVisible();
await expect(page.locator(".layer-row")).toHaveCount(3);
await page.getByRole("button", { name: "+ Add layer" }).click();
await expect(page.locator(".layer-row")).toHaveCount(4);
await expect(page.locator(".result-values code").first()).not.toBeEmpty();
await page.getByRole("tab", { name: /Steps/u }).click();
await expect(
page.getByRole("heading", { name: "Step between colours" }),
).toBeVisible();
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([]);
});
test("keeps picking, accessibility analysis and palette data browser-local", async ({
page,
}) => {
const runtimeErrors = recordRuntimeErrors(page);
const externalRequests = await keepNetworkLocal(page);
await page.goto("/deep/nested/colour/#pick");
await expect(
page.getByRole("heading", { name: "Pick a colour" }),
).toBeVisible();
const picker = page.getByRole("application");
await expect(picker).toHaveAccessibleName(/Saturation 62 percent/u);
await picker.focus();
await picker.press("ArrowRight");
await expect(picker).toHaveAccessibleName(/Saturation 63 percent/u);
await page.getByLabel("Exact CSS colour").fill("oklch(70% 0.2 30)");
await page.getByRole("button", { name: "Add to palette" }).click();
await page.getByRole("tab", { name: /Contrast/u }).click();
await expect(
page.getByRole("heading", { name: "Contrast & compare" }),
).toBeVisible();
await page
.getByRole("textbox", { name: "Foreground", exact: true })
.fill("#000000");
await page
.getByRole("textbox", { name: "Background", exact: true })
.fill("#ffffff");
await expect(page.locator(".ratio-heading strong")).toHaveText("21.00");
await expect(
page.getByText("AAA normal text", { exact: true }),
).toBeVisible();
await page.getByRole("tab", { name: /Palette/u }).click();
await expect(
page.getByRole("heading", { name: "Build a palette" }),
).toBeVisible();
await page
.getByLabel("Palette data")
.fill("primary: #123456\naccent: oklch(76% 0.17 87)");
await page.getByRole("button", { name: "Import colours" }).click();
await expect(page.getByRole("status")).toContainText("Added 2 colours");
await expect(page.locator(".saved-colour-list > li")).toHaveCount(3);
await expect(page.locator(".export-preview")).toContainText(
"--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(4);
expect(externalRequests).toEqual([]);
expect(runtimeErrors).toEqual([]);
});
test("loads, samples and extracts a palette from an in-memory image", async ({
page,
}) => {
const runtimeErrors = recordRuntimeErrors(page);
const externalRequests = await keepNetworkLocal(page);
await page.goto("/deep/nested/colour/#image");
await expect(
page.getByRole("heading", { name: "Pick and extract colours" }),
).toBeVisible();
await page.locator('input[type="file"]').evaluate((element, encoded) => {
const bytes = Uint8Array.from(atob(encoded), (character) =>
character.charCodeAt(0),
);
const transfer = new DataTransfer();
transfer.items.add(
new File([bytes], "four-colours.png", { type: "image/png" }),
);
const input = element as HTMLInputElement;
input.files = transfer.files;
input.dispatchEvent(new Event("change", { bubbles: true }));
}, FOUR_COLOUR_PNG);
await expect(page.locator(".image-picker-lab__status")).toContainText(
"four-colours.png loaded: 2 by 2 pixels",
);
await page.getByRole("button", { name: "Sample coordinate" }).click();
await expect(page.getByLabel("Sampled colour #FF0000")).toBeVisible();
await page.getByRole("button", { name: "Extract palette" }).click();
await expect(
page.getByRole("list", { name: "Extracted image palette" }),
).toBeVisible({ timeout: 30_000 });
expect(
await page
.getByRole("list", { name: "Extracted image palette" })
.getByRole("listitem")
.count(),
).toBeGreaterThanOrEqual(2);
await page.getByRole("button", { name: "Remove image" }).click();
await expect(page.locator(".image-picker-lab__status")).toContainText(
"Image removed",
);
expect(externalRequests).toEqual([]);
expect(runtimeErrors).toEqual([]);
});
test("serves a relocatable production artifact with hardened headers", async ({
request,
}) => {
const index = await request.get("/deep/nested/colour/");
expect(index.ok()).toBe(true);
expect(index.headers()["content-security-policy"]).toContain(
"default-src 'self'",
);
expect(index.headers()["x-content-type-options"]).toBe("nosniff");
expect(await index.text()).not.toMatch(/\b(?:src|href)=["']\//u);
const manifest = await request.get("/deep/nested/colour/toolbox-app.json");
expect(manifest.headers()["content-type"]).toContain("application/json");
await expect(manifest.json()).resolves.toMatchObject({
id: "de.add-ideas.colour-tools",
version: "0.2.0",
entry: "./",
icon: "./favicon.svg",
});
});