mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
fix: resolve 41 bugs and wire 21 unimplemented features in image editor
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
This commit is contained in:
@@ -0,0 +1,131 @@
|
||||
import { createNewDocument, drawOnCanvas, expect, selectTool, test } from "./helpers";
|
||||
|
||||
test.describe("Editor Layer Effects and Blend Modes", () => {
|
||||
test.beforeEach(async ({ editorPage: page }) => {
|
||||
await createNewDocument(page);
|
||||
// Ensure layers tab is active
|
||||
await page.locator("[data-testid='tab-layers']").click();
|
||||
await page.waitForTimeout(300);
|
||||
});
|
||||
|
||||
test("blend mode dropdown exists in layers panel", async ({ editorPage: page }) => {
|
||||
const blendSelect = page.locator("[data-testid='blend-mode-select']");
|
||||
await expect(blendSelect).toBeVisible();
|
||||
|
||||
// Should default to Normal (source-over)
|
||||
await expect(blendSelect).toHaveValue("source-over");
|
||||
});
|
||||
|
||||
test("blend mode can be changed", async ({ editorPage: page }) => {
|
||||
const blendSelect = page.locator("[data-testid='blend-mode-select']");
|
||||
await expect(blendSelect).toBeVisible();
|
||||
|
||||
// Change to Multiply
|
||||
await blendSelect.selectOption("multiply");
|
||||
await page.waitForTimeout(300);
|
||||
|
||||
await expect(blendSelect).toHaveValue("multiply");
|
||||
|
||||
// Change to Screen
|
||||
await blendSelect.selectOption("screen");
|
||||
await page.waitForTimeout(300);
|
||||
|
||||
await expect(blendSelect).toHaveValue("screen");
|
||||
|
||||
// Change to Overlay
|
||||
await blendSelect.selectOption("overlay");
|
||||
await page.waitForTimeout(300);
|
||||
|
||||
await expect(blendSelect).toHaveValue("overlay");
|
||||
});
|
||||
|
||||
test("effects section exists (drop shadow, stroke)", async ({ editorPage: page }) => {
|
||||
test.slow();
|
||||
|
||||
// We need a selected object for the effects section to appear.
|
||||
// Draw something on the canvas to create an object.
|
||||
await selectTool(page, "shape-rect");
|
||||
await drawOnCanvas(page, 100, 100, 300, 250);
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
// Switch to move tool and click the shape to select it
|
||||
await selectTool(page, "move");
|
||||
const canvas = page.locator("canvas").first();
|
||||
const box = await canvas.boundingBox();
|
||||
if (!box) throw new Error("Canvas not found");
|
||||
await page.mouse.click(box.x + 200, box.y + 175);
|
||||
await page.waitForTimeout(300);
|
||||
|
||||
// Switch to layers tab to see effects
|
||||
await page.locator("[data-testid='tab-layers']").click();
|
||||
await page.waitForTimeout(300);
|
||||
|
||||
// The Layer Effects section should be visible
|
||||
const effectsLabel = page.getByText("Layer Effects");
|
||||
await expect(effectsLabel).toBeVisible();
|
||||
|
||||
// Drop Shadow and Stroke labels should exist
|
||||
await expect(page.getByText("Drop Shadow").first()).toBeVisible();
|
||||
await expect(page.getByText("Stroke").first()).toBeVisible();
|
||||
});
|
||||
|
||||
test("drop shadow toggle enables shadow settings", async ({ editorPage: page }) => {
|
||||
test.slow();
|
||||
|
||||
// Create and select a shape object
|
||||
await selectTool(page, "shape-rect");
|
||||
await drawOnCanvas(page, 100, 100, 300, 250);
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
await selectTool(page, "move");
|
||||
const canvas = page.locator("canvas").first();
|
||||
const box = await canvas.boundingBox();
|
||||
if (!box) throw new Error("Canvas not found");
|
||||
await page.mouse.click(box.x + 200, box.y + 175);
|
||||
await page.waitForTimeout(300);
|
||||
|
||||
await page.locator("[data-testid='tab-layers']").click();
|
||||
await page.waitForTimeout(300);
|
||||
|
||||
// Find the Drop Shadow checkbox
|
||||
const dropShadowLabel = page.locator("label").filter({ hasText: "Drop Shadow" });
|
||||
const dropShadowCheckbox = dropShadowLabel.locator("input[type='checkbox']");
|
||||
await dropShadowCheckbox.scrollIntoViewIfNeeded();
|
||||
|
||||
// Initially unchecked
|
||||
await expect(dropShadowCheckbox).not.toBeChecked();
|
||||
|
||||
// Enable drop shadow
|
||||
await dropShadowCheckbox.check();
|
||||
await page.waitForTimeout(300);
|
||||
|
||||
await expect(dropShadowCheckbox).toBeChecked();
|
||||
|
||||
// Expand the section to see controls by clicking the chevron
|
||||
const expandBtn = dropShadowLabel.locator("..").locator("button[aria-label*='Expand']");
|
||||
if (await expandBtn.isVisible()) {
|
||||
await expandBtn.click();
|
||||
await page.waitForTimeout(300);
|
||||
}
|
||||
});
|
||||
|
||||
test("layer opacity slider works", async ({ editorPage: page }) => {
|
||||
const slider = page.locator("[data-testid='layer-opacity-slider']");
|
||||
await expect(slider).toBeVisible();
|
||||
|
||||
// Default opacity should be 100 (full)
|
||||
await expect(slider).toHaveValue("100");
|
||||
|
||||
// Change opacity to 50
|
||||
await slider.fill("50");
|
||||
await page.waitForTimeout(300);
|
||||
|
||||
await expect(slider).toHaveValue("50");
|
||||
|
||||
// Change back to 75
|
||||
await slider.fill("75");
|
||||
await page.waitForTimeout(300);
|
||||
|
||||
await expect(slider).toHaveValue("75");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user