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,109 @@
|
||||
import { createNewDocument, expect, selectTool, test } from "./helpers";
|
||||
|
||||
test.describe("Editor Options Bar", () => {
|
||||
test.beforeEach(async ({ editorPage: page }) => {
|
||||
await createNewDocument(page);
|
||||
});
|
||||
|
||||
test("eyedropper options show sample size when eyedropper selected", async ({
|
||||
editorPage: page,
|
||||
}) => {
|
||||
await selectTool(page, "eyedropper");
|
||||
|
||||
// The options bar should display the Sample label and dropdown
|
||||
await expect(page.getByText("Sample:")).toBeVisible();
|
||||
|
||||
const sampleDropdown = page.locator("[data-testid='sample-size-dropdown']");
|
||||
await expect(sampleDropdown).toBeVisible();
|
||||
|
||||
// Should default to "Point (1x1)"
|
||||
await expect(sampleDropdown).toContainText("Point (1x1)");
|
||||
|
||||
// Clicking the dropdown should reveal size options
|
||||
await sampleDropdown.click();
|
||||
await page.waitForTimeout(300);
|
||||
|
||||
await expect(page.getByText("3x3 Average")).toBeVisible();
|
||||
await expect(page.getByText("5x5 Average")).toBeVisible();
|
||||
});
|
||||
|
||||
test("transform options show position/size when transform selected", async ({
|
||||
editorPage: page,
|
||||
}) => {
|
||||
await selectTool(page, "transform");
|
||||
|
||||
// The options bar should show X, Y, W, H, Rotation inputs
|
||||
await expect(page.locator("#transform-x")).toBeVisible();
|
||||
await expect(page.locator("#transform-y")).toBeVisible();
|
||||
await expect(page.locator("#transform-w")).toBeVisible();
|
||||
await expect(page.locator("#transform-h")).toBeVisible();
|
||||
await expect(page.locator("#transform-rotation")).toBeVisible();
|
||||
|
||||
// Flip buttons should be visible
|
||||
await expect(page.locator("button[aria-label='Flip Horizontal']")).toBeVisible();
|
||||
await expect(page.locator("button[aria-label='Flip Vertical']")).toBeVisible();
|
||||
|
||||
// Aspect ratio lock button should be visible
|
||||
const lockBtn = page.locator("button[aria-label*='aspect ratio']");
|
||||
await expect(lockBtn).toBeVisible();
|
||||
});
|
||||
|
||||
test("brush options show size, opacity, hardness", async ({ editorPage: page }) => {
|
||||
await selectTool(page, "brush");
|
||||
|
||||
const optionsBar = page.locator(".flex.items-center.h-10");
|
||||
|
||||
// Size, Opacity, and Hardness labels should be in the options bar
|
||||
await expect(optionsBar.getByText("Size")).toBeVisible();
|
||||
await expect(optionsBar.getByText("Opacity")).toBeVisible();
|
||||
await expect(optionsBar.getByText("Hardness")).toBeVisible();
|
||||
|
||||
// Each should have a range slider and a number input
|
||||
const sizeSlider = optionsBar
|
||||
.locator("label")
|
||||
.filter({ hasText: "Size" })
|
||||
.locator("input[type='range']");
|
||||
await expect(sizeSlider).toBeVisible();
|
||||
|
||||
const opacitySlider = optionsBar
|
||||
.locator("label")
|
||||
.filter({ hasText: "Opacity" })
|
||||
.locator("input[type='range']");
|
||||
await expect(opacitySlider).toBeVisible();
|
||||
|
||||
const hardnessSlider = optionsBar
|
||||
.locator("label")
|
||||
.filter({ hasText: "Hardness" })
|
||||
.locator("input[type='range']");
|
||||
await expect(hardnessSlider).toBeVisible();
|
||||
});
|
||||
|
||||
test("selection options show mode dropdown", async ({ editorPage: page }) => {
|
||||
await selectTool(page, "marquee-rect");
|
||||
|
||||
// The options bar should show Type and Mode sections
|
||||
await expect(page.getByText("Type:")).toBeVisible();
|
||||
await expect(page.getByText("Mode:")).toBeVisible();
|
||||
|
||||
// Type buttons: Rect, Ellipse, Lasso
|
||||
const rectBtn = page.locator("button[aria-label='Rectangular']");
|
||||
const ellipseBtn = page.locator("button[aria-label='Elliptical']");
|
||||
const lassoBtn = page.locator("button[aria-label='Lasso']");
|
||||
|
||||
await expect(rectBtn).toBeVisible();
|
||||
await expect(ellipseBtn).toBeVisible();
|
||||
await expect(lassoBtn).toBeVisible();
|
||||
|
||||
// Rect should be active (pressed) since we selected marquee-rect
|
||||
await expect(rectBtn).toHaveAttribute("aria-pressed", "true");
|
||||
|
||||
// Mode buttons: New, Add, Sub
|
||||
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();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user