Files
SnapOtter/tests/e2e-editor/editor-selection-tools.spec.ts
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

165 lines
5.9 KiB
TypeScript

import { createNewDocument, drawOnCanvas, expect, selectTool, test } from "./helpers";
test.describe("Editor Selection Tools", () => {
test.beforeEach(async ({ editorPage: page }) => {
await createNewDocument(page);
});
test("rectangle selection creates visible selection area", async ({ editorPage: page }) => {
test.slow();
await selectTool(page, "marquee-rect");
const canvas = page.locator("canvas").first();
const before = await canvas.screenshot();
// Drag to create a rectangular selection
await drawOnCanvas(page, 100, 100, 300, 250);
await page.waitForTimeout(500);
const after = await canvas.screenshot();
// The marching ants overlay should cause a visual difference
expect(Buffer.compare(before, after)).not.toBe(0);
});
test("lasso tool creates selection", async ({ editorPage: page }) => {
test.slow();
// Activate the lasso-free tool
await selectTool(page, "lasso-free");
const canvas = page.locator("canvas").first();
const before = await canvas.screenshot();
// Draw a freehand lasso path (needs enough points to form a polygon)
const box = await canvas.boundingBox();
if (!box) throw new Error("Canvas not found");
await page.mouse.move(box.x + 100, box.y + 100);
await page.mouse.down();
await page.mouse.move(box.x + 200, box.y + 100, { steps: 5 });
await page.mouse.move(box.x + 200, box.y + 200, { steps: 5 });
await page.mouse.move(box.x + 100, box.y + 200, { steps: 5 });
await page.mouse.move(box.x + 100, box.y + 100, { steps: 5 });
await page.mouse.up();
await page.waitForTimeout(500);
const after = await canvas.screenshot();
expect(Buffer.compare(before, after)).not.toBe(0);
});
test("magic wand tool creates selection on click", async ({ editorPage: page }) => {
test.slow();
// Draw something first so the magic wand has varied pixel data
await selectTool(page, "brush");
await drawOnCanvas(page, 100, 100, 300, 300);
await page.waitForTimeout(300);
// Switch to magic wand
await selectTool(page, "magic-wand");
await page.waitForTimeout(300);
// Click a blank area to select it. Use the canvas centre: it always maps
// inside the document, whereas a corner can fall in the checkerboard
// padding around a centred document (out of bounds -> no selection).
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 + box.width / 2, box.y + box.height / 2);
// A selection renders as black+white dashed "marching ants" (Konva nodes
// with a dash and stroke #000000/#ffffff; a wand selection uses Shape
// nodes). The brush stroke is solid (no dash), so it is excluded. The wand
// flood-fill + outline render can take a moment on a large canvas, so poll.
await expect
.poll(
() =>
page.evaluate(() => {
const konva = (
window as unknown as {
Konva?: {
stages: Array<{
find(
selector: string,
): Array<{ stroke(): string; dash(): number[] | undefined }>;
}>;
};
}
).Konva;
if (!konva?.stages?.length) return false;
const stage = konva.stages[0];
const isMarchingAnts = (node: { stroke(): string; dash(): number[] | undefined }) =>
(node.dash()?.length ?? 0) > 0 &&
(node.stroke() === "#000000" || node.stroke() === "#ffffff");
return ["Shape", "Rect", "Ellipse", "Line"].some((cls) =>
stage.find(cls).some(isMarchingAnts),
);
}),
{ timeout: 12000 },
)
.toBe(true);
});
test("selection mode toggle (add/subtract) exists in options bar", async ({
editorPage: page,
}) => {
await selectTool(page, "marquee-rect");
// The options bar should show Mode label
await expect(page.getByText("Mode:")).toBeVisible();
// New, Add, Sub buttons should be visible
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();
// "New" should be active by default
await expect(newBtn).toHaveAttribute("aria-pressed", "true");
});
test("Ctrl+D deselects", async ({ editorPage: page }) => {
test.slow();
// Create a selection first
await selectTool(page, "marquee-rect");
await drawOnCanvas(page, 100, 100, 300, 250);
await page.waitForTimeout(500);
const canvas = page.locator("canvas").first();
const withSelection = await canvas.screenshot();
// Press Ctrl+D to deselect
await page.keyboard.press("Control+d");
await page.waitForTimeout(500);
const afterDeselect = await canvas.screenshot();
// The marching ants should disappear, making a visual difference
expect(Buffer.compare(withSelection, afterDeselect)).not.toBe(0);
});
test("Ctrl+Shift+I inverts selection", async ({ editorPage: page }) => {
test.slow();
// Create a selection first
await selectTool(page, "marquee-rect");
await drawOnCanvas(page, 100, 100, 200, 200);
await page.waitForTimeout(500);
const canvas = page.locator("canvas").first();
const beforeInvert = await canvas.screenshot();
// Press Ctrl+Shift+I to invert selection
await page.keyboard.press("Control+Shift+i");
await page.waitForTimeout(500);
const afterInvert = await canvas.screenshot();
// The selection bounds should change, causing a visual difference
expect(Buffer.compare(beforeInvert, afterInvert)).not.toBe(0);
});
});