Files
SnapOtter/tests/e2e-editor/editor-transform-resize.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

142 lines
5.2 KiB
TypeScript

import { createNewDocument, drawOnCanvas, expect, selectTool, test } from "./helpers";
test.describe("Editor Transform and Resize", () => {
test.beforeEach(async ({ editorPage: page }) => {
await createNewDocument(page);
});
test("resize canvas dialog opens and works", async ({ editorPage: page }) => {
// Right-click on the canvas to open the context menu
const canvas = page.locator("canvas").first();
await canvas.click({ button: "right" });
await page.waitForTimeout(300);
// Click "Canvas Size..." in the context menu
const canvasSizeBtn = page.locator("button").filter({ hasText: "Canvas Size..." });
await expect(canvasSizeBtn).toBeVisible();
await canvasSizeBtn.click();
await page.waitForTimeout(300);
// The Canvas Size dialog should appear
const dialogTitle = page.getByText("Canvas Size", { exact: true });
await expect(dialogTitle).toBeVisible();
// Width and Height inputs should be visible
const widthInput = page.locator("#canvas-w");
const heightInput = page.locator("#canvas-h");
await expect(widthInput).toBeVisible();
await expect(heightInput).toBeVisible();
// Anchor buttons should be present (9-point grid)
const anchorButtons = page.locator("button[aria-label^='Anchor']");
await expect(anchorButtons).toHaveCount(9);
// Background color input should be visible
const bgColorInput = page.locator("#canvas-fill");
await expect(bgColorInput).toBeVisible();
// Apply and Cancel buttons should be present
await expect(page.locator("button").filter({ hasText: "Apply" })).toBeVisible();
await expect(page.locator("button").filter({ hasText: "Cancel" })).toBeVisible();
// Cancel should close the dialog
await page.locator("button").filter({ hasText: "Cancel" }).click();
await page.waitForTimeout(300);
// Dialog should be gone
await expect(dialogTitle).not.toBeVisible();
});
test("resize image dialog opens and works", async ({ editorPage: page }) => {
// Right-click on the canvas to open the context menu
const canvas = page.locator("canvas").first();
await canvas.click({ button: "right" });
await page.waitForTimeout(300);
// Click "Image Size..." in the context menu
const imageSizeBtn = page.locator("button").filter({ hasText: "Image Size..." });
await expect(imageSizeBtn).toBeVisible();
await imageSizeBtn.click();
await page.waitForTimeout(300);
// The Image Size dialog should appear
const dialogTitle = page.getByText("Image Size", { exact: true });
await expect(dialogTitle).toBeVisible();
// Width and Height inputs should be visible
const widthInput = page.locator("#img-w");
const heightInput = page.locator("#img-h");
await expect(widthInput).toBeVisible();
await expect(heightInput).toBeVisible();
// Aspect ratio lock button should be visible
const lockBtn = page.locator("button[aria-label*='aspect ratio']");
await expect(lockBtn).toBeVisible();
// Resampling select should be present
const resampleSelect = page.locator("#resample");
await expect(resampleSelect).toBeVisible();
// It should have the expected options
const options = resampleSelect.locator("option");
const texts = await options.allTextContents();
expect(texts).toContain("Nearest Neighbor (fast)");
expect(texts).toContain("Bicubic (smooth)");
// Cancel should close the dialog
await page.locator("button").filter({ hasText: "Cancel" }).click();
await page.waitForTimeout(300);
await expect(dialogTitle).not.toBeVisible();
});
test("flip horizontal via transform options changes canvas", async ({ editorPage: page }) => {
test.slow();
// Draw an asymmetric shape so flip is visually detectable
await selectTool(page, "brush");
await drawOnCanvas(page, 50, 50, 200, 100);
await page.waitForTimeout(300);
// Switch to transform tool
await selectTool(page, "transform");
await page.waitForTimeout(300);
const canvas = page.locator("canvas").first();
const before = await canvas.screenshot();
// Click the Flip Horizontal button in the transform options bar
const flipHBtn = page.locator("button[aria-label='Flip Horizontal']");
await expect(flipHBtn).toBeVisible();
await flipHBtn.click();
await page.waitForTimeout(500);
const after = await canvas.screenshot();
expect(Buffer.compare(before, after)).not.toBe(0);
});
test("flip vertical via transform options changes canvas", async ({ editorPage: page }) => {
test.slow();
// Draw an asymmetric shape so flip is visually detectable
await selectTool(page, "brush");
await drawOnCanvas(page, 50, 50, 100, 200);
await page.waitForTimeout(300);
// Switch to transform tool
await selectTool(page, "transform");
await page.waitForTimeout(300);
const canvas = page.locator("canvas").first();
const before = await canvas.screenshot();
// Click the Flip Vertical button in the transform options bar
const flipVBtn = page.locator("button[aria-label='Flip Vertical']");
await expect(flipVBtn).toBeVisible();
await flipVBtn.click();
await page.waitForTimeout(500);
const after = await canvas.screenshot();
expect(Buffer.compare(before, after)).not.toBe(0);
});
});