Files
SnapOtter/tests/e2e-editor/editor-transform-resize.spec.ts
T
SnapOtter 3120e6708d fix(editor): apply layer effects + object flip, add Beta badge, repair e2e specs
While getting the editor e2e suite green, three "stale test" failures turned
out to be real bugs (per the reporter's hunch that tests might be catching
real issues):

- Layer effects (drop shadow, glows) never applied. The panel wrote effects
  into `attrs.effects` through updateObject, but the panel and renderer both
  read the object's top-level `effects`, so the toggle never persisted. Add a
  dedicated `setObjectEffects` store action and route the panel through it.
- Object flip (transform tool) did nothing. No object renderer applied
  `scaleX`/`scaleY`, and the flip negated scale without compensating position.
  Apply scale in the renderers and flip in place: mirror points for stroke
  objects, negate scale + shift position for sized objects.

(The paint-bucket / pixel-tool coordinate bug and the broken-at-non-100%-zoom
export were fixed in the preceding #259 change.)

Also adds a small "Beta" badge to the editor (welcome heading + nav link) and
repairs ~18 stale editor e2e specs whose selectors/assertions had drifted from
the current UI: the options bar is `h-9` not `h-10` (added a stable
`data-testid`), the menu bar is `h-8`/`bg-background`, the flip button
aria-labels are lowercase, the welcome "Image Editor" heading collides with an
sr-only `<h1>`, the color-picker tabs need a role-scoped selector, and the
magic-wand / flip tests now use deterministic setup and assert the actual
effect instead of fragile screenshot diffs.
2026-06-17 14:21:35 +08:00

137 lines
5.2 KiB
TypeScript

import { createNewDocument, 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();
// Cancel should close the dialog
await page.locator("button").filter({ hasText: "Cancel" }).click();
await page.waitForTimeout(300);
await expect(dialogTitle).not.toBeVisible();
});
// Paint-fill the canvas, select the resulting image object, and open the
// transform tool. Returns the canvas centre for follow-up clicks.
async function fillAndSelectObject(page: import("@playwright/test").Page) {
await selectTool(page, "fill");
const box = await page.locator("canvas").first().boundingBox();
if (!box) throw new Error("Canvas not found");
const cx = box.x + box.width / 2;
const cy = box.y + box.height / 2;
await page.mouse.click(cx, cy);
await page.waitForTimeout(400);
// Select the fill object with the move tool, then switch to transform.
await selectTool(page, "move");
await page.mouse.click(cx, cy);
await page.waitForTimeout(200);
await selectTool(page, "transform");
await page.waitForTimeout(300);
}
// True when some image object on the stage is mirrored along the given axis.
function anyImageMirrored(page: import("@playwright/test").Page, axis: "x" | "y") {
return page.evaluate((flipAxis) => {
const konva = (
window as unknown as {
Konva?: {
stages: Array<{ find(s: string): Array<{ scaleX(): number; scaleY(): number }> }>;
};
}
).Konva;
if (!konva?.stages?.length) return false;
return konva.stages[0]
.find("Image")
.some((node) => (flipAxis === "x" ? node.scaleX() : node.scaleY()) < 0);
}, axis);
}
test("flip horizontal mirrors the selected object", async ({ editorPage: page }) => {
test.slow();
await fillAndSelectObject(page);
expect(await anyImageMirrored(page, "x")).toBe(false);
await page.locator("button[aria-label='Flip horizontal']").click();
await page.waitForTimeout(400);
expect(await anyImageMirrored(page, "x")).toBe(true);
});
test("flip vertical mirrors the selected object", async ({ editorPage: page }) => {
test.slow();
await fillAndSelectObject(page);
expect(await anyImageMirrored(page, "y")).toBe(false);
await page.locator("button[aria-label='Flip vertical']").click();
await page.waitForTimeout(400);
expect(await anyImageMirrored(page, "y")).toBe(true);
});
});