202 lines
7.1 KiB
TypeScript
202 lines
7.1 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);
|
|
|
|
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.reload();
|
|
await page.getByRole("tab", { name: /Palette/u }).click();
|
|
await expect(page.locator(".saved-colour-list > li")).toHaveCount(3);
|
|
|
|
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.1.0",
|
|
entry: "./",
|
|
icon: "./favicon.svg",
|
|
});
|
|
});
|