Release Scan Tools v0.1.0
This commit is contained in:
@@ -0,0 +1,201 @@
|
||||
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();
|
||||
});
|
||||
Reference in New Issue
Block a user