mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
Canvas rendering: - Fix Konva filter application order (filters before cache) - Implement 6 missing filters (motionBlur, radialBlur, surfaceBlur, vignette, grain, sharpen) - Implement exposure, vibrance, warmth adjustments as custom Konva filters - Apply layer blend modes via globalCompositeOperation - Apply object effects (drop shadow, outer glow, stroke) to all shapes - Mount SmartGuidesOverlay during move tool drag - Clip pixel grid to visible viewport (200-line cap for performance) Store logic: - resizeImage now scales all objects proportionally (points, radii, fontSize) - rotate/flip/trim handle line/arrow points arrays and center-based objects - applyCrop creates cropped source image via offscreen canvas - invertSelection creates mask from bounds when no mask exists - cutObjects uses single atomic set() to prevent race conditions - sendToBack respects layer ordering in multi-layer documents - Add batchNudge() and commitHistory() for undoable nudge operations - Add updateLayerThumbnail() method Tool hooks: - Fix clone stamp/dodge/burn perf (toDataURL only on mouseUp, not every move) - Fix magic wand zoom/pixelRatio with explicit stage.toCanvas() viewport - Fix eyedropper sampling with unzoomed canvas export - Fix selection tool stale closure via isDrawingRef - Implement polygonal lasso (click-to-place vertices, double-click to close) - Implement selection subtract mode (geometric and mask-based) - Implement gradient live preview during drag - Fix transform/move tool to persist changes and handle ellipse/polygon/star UI wiring: - Mount rulers and guidelines in editor page - Wire histogram with live canvas imageData - Wire autosave recovery with blob-to-dataURL conversion - Wire fill dialog to Shift+Backspace shortcut - Wire eyedropper and transform options to options bar - Fix history panel undo/redo button reactive state via useSyncExternalStore - Fix layer row name click to select layer (timer-based click/dblclick) - Fix zoom animation coordinate drift with progressive store sync - Fix copy merged to use Konva stage composite export Tests: - 49 new unit tests (store fixes + konva filters) - 8 new E2E test files with 39 test cases
138 lines
4.7 KiB
TypeScript
138 lines
4.7 KiB
TypeScript
import { createNewDocument, drawOnCanvas, expect, selectTool, test } from "./helpers";
|
|
|
|
test.describe("Editor Selection Tools", () => {
|
|
test.beforeEach(async ({ editorPage: page }) => {
|
|
await createNewDocument(page);
|
|
});
|
|
|
|
test("rectangle selection creates visible selection area", async ({ editorPage: page }) => {
|
|
test.slow();
|
|
|
|
await selectTool(page, "marquee-rect");
|
|
|
|
const canvas = page.locator("canvas").first();
|
|
const before = await canvas.screenshot();
|
|
|
|
// Drag to create a rectangular selection
|
|
await drawOnCanvas(page, 100, 100, 300, 250);
|
|
await page.waitForTimeout(500);
|
|
|
|
const after = await canvas.screenshot();
|
|
// The marching ants overlay should cause a visual difference
|
|
expect(Buffer.compare(before, after)).not.toBe(0);
|
|
});
|
|
|
|
test("lasso tool creates selection", async ({ editorPage: page }) => {
|
|
test.slow();
|
|
|
|
// Activate the lasso-free tool
|
|
await selectTool(page, "lasso-free");
|
|
|
|
const canvas = page.locator("canvas").first();
|
|
const before = await canvas.screenshot();
|
|
|
|
// Draw a freehand lasso path (needs enough points to form a polygon)
|
|
const box = await canvas.boundingBox();
|
|
if (!box) throw new Error("Canvas not found");
|
|
|
|
await page.mouse.move(box.x + 100, box.y + 100);
|
|
await page.mouse.down();
|
|
await page.mouse.move(box.x + 200, box.y + 100, { steps: 5 });
|
|
await page.mouse.move(box.x + 200, box.y + 200, { steps: 5 });
|
|
await page.mouse.move(box.x + 100, box.y + 200, { steps: 5 });
|
|
await page.mouse.move(box.x + 100, box.y + 100, { steps: 5 });
|
|
await page.mouse.up();
|
|
await page.waitForTimeout(500);
|
|
|
|
const after = await canvas.screenshot();
|
|
expect(Buffer.compare(before, after)).not.toBe(0);
|
|
});
|
|
|
|
test("magic wand tool creates selection on click", async ({ editorPage: page }) => {
|
|
test.slow();
|
|
|
|
// Draw something first so the magic wand has varied pixel data
|
|
await selectTool(page, "brush");
|
|
await drawOnCanvas(page, 100, 100, 300, 300);
|
|
await page.waitForTimeout(300);
|
|
|
|
// Switch to magic wand
|
|
await selectTool(page, "magic-wand");
|
|
await page.waitForTimeout(300);
|
|
|
|
const canvas = page.locator("canvas").first();
|
|
const before = await canvas.screenshot();
|
|
|
|
// Click on a blank area to select it
|
|
const box = await canvas.boundingBox();
|
|
if (!box) throw new Error("Canvas not found");
|
|
await page.mouse.click(box.x + 50, box.y + 50);
|
|
await page.waitForTimeout(500);
|
|
|
|
const after = await canvas.screenshot();
|
|
// The magic wand should create a selection (marching ants visible)
|
|
expect(Buffer.compare(before, after)).not.toBe(0);
|
|
});
|
|
|
|
test("selection mode toggle (add/subtract) exists in options bar", async ({
|
|
editorPage: page,
|
|
}) => {
|
|
await selectTool(page, "marquee-rect");
|
|
|
|
// The options bar should show Mode label
|
|
await expect(page.getByText("Mode:")).toBeVisible();
|
|
|
|
// New, Add, Sub buttons should be visible
|
|
const newBtn = page.locator("button[aria-label='New Selection']");
|
|
const addBtn = page.locator("button[aria-label='Add to Selection']");
|
|
const subBtn = page.locator("button[aria-label='Subtract from Selection']");
|
|
|
|
await expect(newBtn).toBeVisible();
|
|
await expect(addBtn).toBeVisible();
|
|
await expect(subBtn).toBeVisible();
|
|
|
|
// "New" should be active by default
|
|
await expect(newBtn).toHaveAttribute("aria-pressed", "true");
|
|
});
|
|
|
|
test("Ctrl+D deselects", async ({ editorPage: page }) => {
|
|
test.slow();
|
|
|
|
// Create a selection first
|
|
await selectTool(page, "marquee-rect");
|
|
await drawOnCanvas(page, 100, 100, 300, 250);
|
|
await page.waitForTimeout(500);
|
|
|
|
const canvas = page.locator("canvas").first();
|
|
const withSelection = await canvas.screenshot();
|
|
|
|
// Press Ctrl+D to deselect
|
|
await page.keyboard.press("Control+d");
|
|
await page.waitForTimeout(500);
|
|
|
|
const afterDeselect = await canvas.screenshot();
|
|
// The marching ants should disappear, making a visual difference
|
|
expect(Buffer.compare(withSelection, afterDeselect)).not.toBe(0);
|
|
});
|
|
|
|
test("Ctrl+Shift+I inverts selection", async ({ editorPage: page }) => {
|
|
test.slow();
|
|
|
|
// Create a selection first
|
|
await selectTool(page, "marquee-rect");
|
|
await drawOnCanvas(page, 100, 100, 200, 200);
|
|
await page.waitForTimeout(500);
|
|
|
|
const canvas = page.locator("canvas").first();
|
|
const beforeInvert = await canvas.screenshot();
|
|
|
|
// Press Ctrl+Shift+I to invert selection
|
|
await page.keyboard.press("Control+Shift+i");
|
|
await page.waitForTimeout(500);
|
|
|
|
const afterInvert = await canvas.screenshot();
|
|
// The selection bounds should change, causing a visual difference
|
|
expect(Buffer.compare(beforeInvert, afterInvert)).not.toBe(0);
|
|
});
|
|
});
|