Files
image-tools/tests/browser/app.spec.ts
T
2026-09-01 02:47:11 +02:00

163 lines
5.5 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 { expect, test, type Page } from "@playwright/test";
import { Buffer } from "node:buffer";
import { VALID_PNG_BASE64, pngFixture } from "../fixtures/images";
const ORIGIN = "http://127.0.0.1:4173";
async function localOnly(page: Page) {
const external: string[] = [];
await page.route("**/*", async (route) => {
const url = new URL(route.request().url());
if (url.origin !== ORIGIN) {
external.push(url.href);
await route.abort();
} else await route.continue();
});
return external;
}
test("runs from a nested path without external requests", async ({ page }) => {
const errors: string[] = [];
page.on("pageerror", (error) => errors.push(error.message));
page.on("console", (message) => {
if (message.type() === "error") errors.push(message.text());
});
const external = await localOnly(page);
await page.goto("/deep/nested/image/");
await expect(
page.getByRole("heading", { name: "Image Tools" }),
).toBeVisible();
expect(external).toEqual([]);
expect(errors).toEqual([]);
});
test("serves the release identity and hardened headers", async ({
request,
}) => {
const index = await request.get("/deep/nested/image/");
expect(index.ok()).toBe(true);
expect(index.headers()["content-security-policy"]).toContain(
"default-src 'self'",
);
expect(await index.text()).not.toMatch(/\b(?:src|href)=["']\//u);
const manifest = await request.get("/deep/nested/image/toolbox-app.json");
await expect(manifest.json()).resolves.toMatchObject({
id: "de.add-ideas.image-tools",
version: "0.1.0",
entry: "./",
});
});
test("inspects, previews, resizes and exports a static PNG locally", async ({
page,
}) => {
const errors: string[] = [];
page.on("pageerror", (error) => errors.push(error.message));
const external = await localOnly(page);
await page.goto("/deep/nested/image/");
await page.locator('input[type="file"]').setInputFiles({
name: "tiny.png",
mimeType: "image/png",
buffer: Buffer.from(VALID_PNG_BASE64, "base64"),
});
await expect(page.getByRole("heading", { name: "tiny.png" })).toBeVisible();
await expect(
page.getByRole("img", { name: "tiny.png, edited preview" }),
).toBeVisible();
await page.getByLabel("Mode").selectOption("exact");
await page.getByLabel("Width px").fill("4");
await page.getByLabel("Height px").fill("3");
await expect(page.getByText("Export 4 × 3")).toBeVisible();
await expect(
page.getByRole("img", { name: "tiny.png, edited preview" }),
).toBeVisible();
const downloadPromise = page.waitForEvent("download");
await page.getByRole("button", { name: "Export current" }).click();
const download = await downloadPromise;
expect(download.suggestedFilename()).toBe("tiny-edited.png");
await expect(page.getByText("Last export report")).toBeVisible();
expect(external).toEqual([]);
expect(errors).toEqual([]);
});
test("rejects an animated PNG without decoding a frame", async ({ page }) => {
await page.goto("/deep/nested/image/");
await page.locator('input[type="file"]').setInputFiles({
name: "animated.png",
mimeType: "image/png",
buffer: Buffer.from(pngFixture(8, 8, { animated: true })),
});
await expect(
page.getByText(/Animated and multi-picture images are not processed/u),
).toBeVisible();
await expect(page.getByRole("img")).toHaveCount(0);
});
test("uses exact native JPEG and WebP codec capabilities", async ({ page }) => {
const external = await localOnly(page);
await page.goto("/deep/nested/image/");
const jpeg = await browserRaster(page, "image/jpeg");
const webp = await browserRaster(page, "image/webp");
expect(jpeg.mimeType).toBe("image/jpeg");
expect(webp.mimeType).toBe("image/webp");
await page.locator('input[type="file"]').setInputFiles([
{
name: "sample.jpg",
mimeType: jpeg.mimeType,
buffer: Buffer.from(jpeg.bytes),
},
{
name: "sample.webp",
mimeType: webp.mimeType,
buffer: Buffer.from(webp.bytes),
},
]);
await expect(page.getByRole("heading", { name: "sample.jpg" })).toBeVisible();
await page.getByLabel("Format").selectOption("jpeg");
let downloadPromise = page.waitForEvent("download");
await page.getByRole("button", { name: "Export current" }).click();
expect((await downloadPromise).suggestedFilename()).toBe("sample-edited.jpg");
await page
.locator(".queue-select")
.filter({ hasText: "sample.webp" })
.click();
await expect(
page.getByRole("heading", { name: "sample.webp" }),
).toBeVisible();
await page.getByLabel("Format").selectOption("webp");
downloadPromise = page.waitForEvent("download");
await page.getByRole("button", { name: "Export current" }).click();
expect((await downloadPromise).suggestedFilename()).toBe(
"sample-edited.webp",
);
expect(external).toEqual([]);
});
async function browserRaster(page: Page, type: "image/jpeg" | "image/webp") {
return page.evaluate(async (mimeType) => {
const canvas = document.createElement("canvas");
canvas.width = 4;
canvas.height = 3;
const context = canvas.getContext("2d");
if (!context) throw new Error("No canvas context");
context.fillStyle = "#7c4dff";
context.fillRect(0, 0, 2, 3);
context.fillStyle = "#19a974";
context.fillRect(2, 0, 2, 3);
const blob = await new Promise<Blob>((resolve, reject) =>
canvas.toBlob(
(value) => (value ? resolve(value) : reject(new Error("No encoder"))),
mimeType,
0.9,
),
);
return {
mimeType: blob.type,
bytes: Array.from(new Uint8Array(await blob.arrayBuffer())),
};
}, type);
}