202 lines
7.1 KiB
TypeScript
202 lines
7.1 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,
|
|
);
|
|
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);
|
|
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.1.0",
|
|
requirements: { workers: true },
|
|
privacy: { processing: "local", telemetry: false },
|
|
});
|
|
await context.setOffline(true);
|
|
await page.reload();
|
|
await expect(page.getByRole("heading", { name: "Scan Tools" })).toBeVisible();
|
|
});
|