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
132 lines
4.4 KiB
TypeScript
132 lines
4.4 KiB
TypeScript
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");
|
|
});
|
|
});
|