Files
privacy-tools/tests/browser/app.spec.ts
T
2026-09-01 02:39:44 +02:00

167 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 { pngFixture, tiffFixture } 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/privacy/");
await expect(
page.getByRole("heading", { name: "Privacy 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/privacy/");
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/privacy/toolbox-app.json");
await expect(manifest.json()).resolves.toMatchObject({
id: "de.add-ideas.privacy-tools",
version: "0.1.0",
entry: "./",
});
});
test("inspects and independently verifies a re-encoded PNG without network access", 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/privacy/");
await page.locator('input[type="file"]').setInputFiles([
{
name: "metadata-fixture.png",
mimeType: "image/png",
buffer: Buffer.from(pngFixture()),
},
{
name: "inventory-only.pdf",
mimeType: "application/pdf",
buffer: Buffer.from("%PDF-1.7\n% fixture"),
},
]);
await expect(
page.getByRole("heading", { name: "Batch inventory" }),
).toBeVisible();
await expect(page.getByText("Alice PNG").first()).toBeVisible();
await expect(page.getByText("PNG Alice").first()).toBeVisible();
await expect(page.getByText("inventory-only.pdf").first()).toBeVisible();
await page.getByRole("button", { name: "Re-encode & verify" }).click();
await expect(page.getByText("Mandatory output re-scan")).toBeVisible();
await expect(page.locator(".verification")).not.toHaveClass(/is-failed/u);
await expect(
page.getByRole("button", { name: "Download re-encoded output" }),
).toBeEnabled();
const imageDownload = page.waitForEvent("download");
await page
.getByRole("button", { name: "Download re-encoded output" })
.click();
expect((await imageDownload).suggestedFilename()).toBe(
"metadata-fixture.clean.png",
);
const reportDownload = page.waitForEvent("download");
await page.getByRole("button", { name: "Download JSON report" }).click();
expect((await reportDownload).suggestedFilename()).toBe(
"privacy-tools-report.json",
);
const archiveDownload = page.waitForEvent("download");
await page
.getByRole("button", { name: "Download 1 re-encoded image + report" })
.click();
expect((await archiveDownload).suggestedFilename()).toBe(
"privacy-tools-re-encoded-images.zip",
);
expect(external).toEqual([]);
expect(errors).toEqual([]);
});
test("normalizes EXIF orientation while re-encoding JPEG pixels", async ({
page,
}) => {
const external = await localOnly(page);
await page.goto("/deep/nested/privacy/");
const encoded = await page.evaluate(async () => {
const canvas = document.createElement("canvas");
canvas.width = 2;
canvas.height = 1;
const context = canvas.getContext("2d");
if (!context) throw new Error("Missing test canvas");
context.fillStyle = "#ff0000";
context.fillRect(0, 0, 1, 1);
context.fillStyle = "#0000ff";
context.fillRect(1, 0, 1, 1);
const blob = await new Promise<Blob>((resolve, reject) =>
canvas.toBlob(
(value) =>
value ? resolve(value) : reject(new Error("JPEG encode failed")),
"image/jpeg",
0.95,
),
);
return [...new Uint8Array(await blob.arrayBuffer())];
});
const jpeg = addExifOrientation(Buffer.from(encoded), 6);
await page.locator('input[type="file"]').setInputFiles({
name: "oriented.jpg",
mimeType: "image/jpeg",
buffer: jpeg,
});
await expect(page.getByText("2 × 1 pixels")).toBeVisible();
await page.getByRole("button", { name: "Re-encode & verify" }).click();
await expect(page.getByText("Mandatory output re-scan")).toBeVisible();
await expect(page.locator(".verification")).not.toHaveClass(/is-failed/u);
await expect(page.getByText("Normalized", { exact: true })).toBeVisible();
expect(external).toEqual([]);
});
function addExifOrientation(jpeg: Buffer, orientation: number): Buffer {
const payload = Buffer.concat([
Buffer.from("Exif\0\0", "binary"),
Buffer.from(tiffFixture({ orientation })),
]);
const segment = Buffer.from([
0xff,
0xe1,
((payload.length + 2) >>> 8) & 0xff,
(payload.length + 2) & 0xff,
]);
return Buffer.concat([
jpeg.subarray(0, 2),
segment,
payload,
jpeg.subarray(2),
]);
}