Files
SnapOtter/tests/e2e-editor/editor-layer-effects.spec.ts
T
SnapOtter dd73a8a50a 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
2026-05-08 16:43:27 +08:00

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");
});
});