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(); });