mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
feat(web): in-canvas zoom & pan for the object eraser and split tools (#320)
* feat(web): add pure zoom/pan math module with unit tests * feat(i18n): add a11y.pan key across all locales (English, matching adjacent zoom labels) * feat(web): add useZoomPan hook (state + gestures over pure math) * feat(web): add ZoomToolbar component * feat(web): zoom & pan in the object eraser canvas * feat(web): zoom & pan in the split tool preview * fix(web): synchronous pan-mode refs so drag-pan is race-free under fast input * test(e2e): zoom & pan acceptance (split always-on, eraser bundle-gated)
This commit is contained in:
@@ -0,0 +1,83 @@
|
||||
import path from "node:path";
|
||||
import { expect, test } from "./helpers";
|
||||
|
||||
function fixturePath(name: string): string {
|
||||
return path.join(process.cwd(), "tests", "fixtures", name);
|
||||
}
|
||||
|
||||
async function uploadFile(page: import("@playwright/test").Page, filePath: string) {
|
||||
const fileChooserPromise = page.waitForEvent("filechooser");
|
||||
const dropzone = page.locator("[class*='border-dashed']").first();
|
||||
await dropzone.click();
|
||||
const fileChooser = await fileChooserPromise;
|
||||
await fileChooser.setFiles(filePath);
|
||||
await page.waitForTimeout(500);
|
||||
}
|
||||
|
||||
test.describe("In-canvas zoom & pan", () => {
|
||||
test("split: toolbar, wheel zoom, and hand-tool drag pan", async ({ loggedInPage: page }) => {
|
||||
await page.goto("/image/split");
|
||||
await uploadFile(page, fixturePath("image/valid/test-200x150.png"));
|
||||
|
||||
const viewport = page.getByTestId("zoom-viewport");
|
||||
const content = page.getByTestId("zoom-content");
|
||||
const percent = page.getByTestId("zoom-percent");
|
||||
await expect(page.getByTestId("zoom-toolbar")).toBeVisible();
|
||||
await content.waitFor({ state: "visible", timeout: 5_000 });
|
||||
|
||||
const box = await viewport.boundingBox();
|
||||
if (!box) throw new Error("no viewport box");
|
||||
const cx = box.x + box.width / 2;
|
||||
const cy = box.y + box.height / 2;
|
||||
|
||||
// Wheel up over the canvas zooms toward the cursor (large delta -> near max zoom).
|
||||
await page.mouse.move(cx, cy);
|
||||
await page.mouse.wheel(0, -3000);
|
||||
await page.waitForTimeout(150);
|
||||
await expect(percent).not.toHaveText("100%");
|
||||
expect(await content.getAttribute("style")).toMatch(/scale\((?!1\))/); // scale != 1
|
||||
|
||||
// Hand-tool toggle, then drag pans (deterministic; translate becomes non-zero).
|
||||
await page.getByTestId("zoom-pan").click();
|
||||
await page.mouse.move(cx, cy);
|
||||
await page.mouse.down();
|
||||
await page.mouse.move(cx - 80, cy - 50, { steps: 10 });
|
||||
await page.mouse.up();
|
||||
await page.waitForTimeout(150);
|
||||
expect(await content.getAttribute("style")).toMatch(/translate\(\s*-?[1-9][0-9.]*px/);
|
||||
});
|
||||
|
||||
test("eraser: zoom in, then draw a stroke while zoomed", async ({ loggedInPage: page }) => {
|
||||
await page.goto("/image/erase-object");
|
||||
// Skip if the AI bundle isn't installed (no masking canvas to draw on).
|
||||
try {
|
||||
await page.getByTestId("erase-object-submit").waitFor({ state: "visible", timeout: 15_000 });
|
||||
} catch {
|
||||
test.skip(true, "object-eraser-colorize feature bundle not installed");
|
||||
}
|
||||
await uploadFile(page, fixturePath("image/valid/test-200x150.png"));
|
||||
|
||||
const viewport = page.getByTestId("zoom-viewport");
|
||||
const content = page.getByTestId("zoom-content");
|
||||
await content.waitFor({ state: "visible", timeout: 5_000 });
|
||||
const box = await viewport.boundingBox();
|
||||
if (!box) throw new Error("no viewport box");
|
||||
const cx = box.x + box.width / 2;
|
||||
const cy = box.y + box.height / 2;
|
||||
|
||||
// Zoom in.
|
||||
await page.mouse.move(cx, cy);
|
||||
await page.mouse.wheel(0, -1200);
|
||||
await page.waitForTimeout(150);
|
||||
expect(await content.getAttribute("style")).toMatch(/scale\((?!1\))/);
|
||||
|
||||
// Draw a stroke while zoomed (coordinate mapping must stay correct under the transform).
|
||||
await page.mouse.down();
|
||||
await page.mouse.move(cx + 20, cy + 10);
|
||||
await page.mouse.move(cx + 40, cy + 20);
|
||||
await page.mouse.up();
|
||||
|
||||
await expect(page.getByRole("button", { name: "Undo" })).toBeVisible();
|
||||
await expect(page.getByTestId("erase-object-submit")).toBeEnabled();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user