Files
scan-tools/tests/browser/app.spec.ts
T
zemion 8f34d05b75
Verify / verify (push) Canceled after 0s
Release Scan Tools 0.2.0
2026-09-02 10:57:17 +02:00

218 lines
7.6 KiB
TypeScript

import { expect, test, type Page } from "@playwright/test";
import { readFile } from "node:fs/promises";
const ORIGIN = "http://127.0.0.1:4203";
async function watchLocalOnly(page: Page) {
const external: string[] = [];
await page.route("**/*", async (route) => {
const url = new URL(route.request().url());
if (url.protocol.startsWith("http") && url.origin !== ORIGIN) {
external.push(url.href);
await route.abort();
} else await route.continue();
});
return external;
}
async function generatedPage(
page: Page,
name = "page.png",
text = "LOCAL SCAN",
) {
const base64 = await page.evaluate((label) => {
const canvas = document.createElement("canvas");
canvas.width = 600;
canvas.height = 800;
const context = canvas.getContext("2d")!;
context.fillStyle = "white";
context.fillRect(0, 0, 600, 800);
context.strokeStyle = "#111";
context.lineWidth = 4;
context.strokeRect(35, 45, 530, 700);
context.fillStyle = "black";
context.font = "bold 52px sans-serif";
context.fillText(label, 95, 210);
context.font = "32px sans-serif";
context.fillText("Processed only in this browser", 65, 300);
return canvas.toDataURL("image/png").split(",")[1]!;
}, text);
return { name, mimeType: "image/png", buffer: Buffer.from(base64, "base64") };
}
test("loads the nested local-first empty workspace", async ({ page }) => {
await page.setViewportSize({ width: 1800, height: 1000 });
const external = await watchLocalOnly(page);
await page.goto("/deep/nested/scan-tools/");
await expect(page.getByRole("heading", { name: "Scan Tools" })).toBeVisible();
await expect(
page.getByRole("heading", { name: "No pages yet" }),
).toBeVisible();
expect(external).toEqual([]);
expect(
await page
.locator(".toolbox-shell__main")
.evaluate((node) => getComputedStyle(node).width),
).toBe("1440px");
});
test("adds and corrects a real local image while retaining the previous preview", async ({
page,
}) => {
await page.goto("/deep/nested/scan-tools/");
await page
.getByLabel("Add images or camera pages")
.setInputFiles(await generatedPage(page));
await expect(page.getByRole("heading", { name: "page.png" })).toBeVisible();
const preview = page.getByLabel("Processed preview of page.png");
await expect
.poll(() => preview.evaluate((node: HTMLCanvasElement) => node.width))
.toBeGreaterThan(100);
const before = await preview.evaluate((node: HTMLCanvasElement) =>
node.toDataURL(),
);
await page.getByLabel("Brightness adjustment").fill("30");
await expect
.poll(() => preview.evaluate((node: HTMLCanvasElement) => node.toDataURL()))
.not.toBe(before);
await page
.getByRole("slider", { name: "Corner 1" })
.press("Shift+ArrowRight");
await expect(page.getByRole("slider", { name: "Corner 1" })).toHaveAttribute(
"aria-valuetext",
/^1% from left/u,
);
await page.getByLabel("Corner 2 X (%)").fill("98.5");
await expect(page.getByRole("slider", { name: "Corner 2" })).toHaveAttribute(
"aria-valuetext",
/^99% from left/u,
);
const download = page.waitForEvent("download");
await page.getByRole("button", { name: "Download PNG" }).click();
expect((await download).suggestedFilename()).toBe("page-scan.png");
});
test("assembles, reorders and removes pages", async ({ page }) => {
await page.goto("/deep/nested/scan-tools/");
await page
.getByLabel("Add images or camera pages")
.setInputFiles([
await generatedPage(page, "first.png", "FIRST"),
await generatedPage(page, "second.png", "SECOND"),
]);
const cards = page.locator(".page-strip li");
await expect(cards).toHaveCount(2);
await cards.nth(0).dragTo(cards.nth(1));
await expect(cards.nth(0)).toContainText("second.png");
await page.getByRole("button", { name: "Remove page 1" }).click();
await expect(cards).toHaveCount(1);
});
test("exports a real image-only PDF", async ({ page }) => {
await page.goto("/deep/nested/scan-tools/");
await page
.getByLabel("Add images or camera pages")
.setInputFiles(await generatedPage(page));
const pending = page.waitForEvent("download");
await page.getByRole("button", { name: "Download 1-page PDF" }).click();
const download = await pending;
expect(download.suggestedFilename()).toBe("local-scan.pdf");
const path = await download.path();
expect(path).toBeTruthy();
expect((await readFile(path!)).subarray(0, 5).toString()).toBe("%PDF-");
});
test("serves the complete OCR runtime from the app origin", async ({
page,
request,
}) => {
const external = await watchLocalOnly(page);
await page.goto("/deep/nested/scan-tools/");
for (const path of [
"ocr/worker.min.js",
"ocr/core/tesseract-core-lstm.wasm.js",
"ocr/core/tesseract-core-simd-lstm.wasm.js",
"ocr/core/tesseract-core-relaxedsimd-lstm.wasm.js",
"ocr/lang/eng.traineddata.gz",
]) {
const response = await request.get(`/deep/nested/scan-tools/${path}`);
expect(response.ok(), path).toBe(true);
expect((await response.body()).byteLength, path).toBeGreaterThan(100_000);
}
expect(external).toEqual([]);
});
test("runs the bundled OCR engine without an external request", async ({
page,
browserName,
}) => {
test.skip(
browserName !== "chromium",
"The engine path is exercised once; Firefox is covered by the asset, CSP, and UI matrix.",
);
const external = await watchLocalOnly(page);
await page.goto("/deep/nested/scan-tools/");
await page
.getByLabel("Add images or camera pages")
.setInputFiles(await generatedPage(page, "ocr.png", "LOCAL SCAN"));
await page.getByLabel("I understand the OCR memory cost.").check();
await page.getByRole("button", { name: "Recognize selected page" }).click();
await expect(page.getByLabel("Recognized text")).toBeVisible({
timeout: 90_000,
});
await expect(page.getByLabel("Recognized text")).toContainText(/LOCAL|SCAN/u);
await expect(page.getByLabel(/positioned OCR words/u)).toBeVisible();
await expect(
page.getByRole("button", { name: "Download hOCR geometry" }),
).toBeVisible();
await expect(
page.getByRole("button", { name: "Download OCR TSV" }),
).toBeVisible();
expect(external).toEqual([]);
});
test("integrates help, themes, identity, headers and offline reload", async ({
page,
request,
context,
}) => {
await page.goto("/deep/nested/scan-tools/");
await page.getByRole("button", { name: "Help" }).click();
await expect(
page.getByRole("dialog", { name: "About Scan Tools" }),
).toContainText("image-only");
await page.keyboard.press("Escape");
await page.getByRole("button", { name: "Personalize" }).click();
await page.getByRole("button", { name: "Dark" }).click();
await expect(page.locator(".toolbox-shell").first()).toHaveAttribute(
"data-toolbox-theme",
"dark",
);
expect(
await page.evaluate(async () =>
Boolean(await navigator.serviceWorker.ready),
),
).toBe(true);
const index = await request.get("/deep/nested/scan-tools/");
expect(index.headers()["content-security-policy"]).toContain(
"worker-src 'self' blob:",
);
expect(await index.text()).not.toMatch(/\b(?:src|href)=["']\//u);
const manifest = await request.get(
"/deep/nested/scan-tools/toolbox-app.json",
);
await expect(manifest.json()).resolves.toMatchObject({
id: "de.add-ideas.scan-tools",
version: "0.2.0",
requirements: { workers: false },
capabilities: {
required: [],
optional: expect.arrayContaining(["workers"]),
},
privacy: { processing: "local", telemetry: false },
});
await context.setOffline(true);
await page.reload();
await expect(page.getByRole("heading", { name: "Scan Tools" })).toBeVisible();
});