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

129 lines
4.7 KiB
TypeScript

import { createNewDocument, drawOnCanvas, expect, selectTool, test } from "./helpers";
test.describe("Editor Drawing Tools", () => {
test.beforeEach(async ({ editorPage: page }) => {
await createNewDocument(page);
});
test("brush activates via toolbar click", async ({ editorPage: page }) => {
await selectTool(page, "brush");
const brushBtn = page.locator("[data-tool='brush']");
await expect(brushBtn).toHaveAttribute("data-tool-active", "true");
});
test("brush activates via B shortcut", async ({ editorPage: page }) => {
// Start with a different tool
await selectTool(page, "move");
await expect(page.locator("[data-tool='move']")).toHaveAttribute("data-tool-active", "true");
// Press B to switch to brush
await page.keyboard.press("b");
await page.waitForTimeout(300);
await expect(page.locator("[data-tool='brush']")).toHaveAttribute("data-tool-active", "true");
});
test("brush options bar shows size, opacity, and hardness", async ({ editorPage: page }) => {
await selectTool(page, "brush");
// Options bar should display Size, Opacity, and Hardness labels
const optionsBar = page.locator('[data-testid="editor-options-bar"]');
await expect(optionsBar.getByText("Size")).toBeVisible();
await expect(optionsBar.getByText("Opacity")).toBeVisible();
await expect(optionsBar.getByText("Hardness")).toBeVisible();
});
test("brush drawing changes canvas content", async ({ editorPage: page }) => {
await selectTool(page, "brush");
const canvas = page.locator("canvas").first();
const before = await canvas.screenshot();
await drawOnCanvas(page, 100, 100, 300, 300);
const after = await canvas.screenshot();
expect(Buffer.compare(before, after)).not.toBe(0);
});
test("shape tool draws rectangle on canvas", async ({ editorPage: page }) => {
await selectTool(page, "shape-rect");
const canvas = page.locator("canvas").first();
const before = await canvas.screenshot();
await drawOnCanvas(page, 100, 100, 250, 200);
const after = await canvas.screenshot();
expect(Buffer.compare(before, after)).not.toBe(0);
});
test("shape tool draws ellipse on canvas", async ({ editorPage: page }) => {
await selectTool(page, "shape-rect");
// Switch to ellipse via the shape type dropdown in options bar
const shapeSelect = page.locator("select").filter({ hasText: "Rectangle" });
await shapeSelect.selectOption("shape-ellipse");
await page.waitForTimeout(300);
const canvas = page.locator("canvas").first();
const before = await canvas.screenshot();
await drawOnCanvas(page, 150, 150, 350, 300);
const after = await canvas.screenshot();
expect(Buffer.compare(before, after)).not.toBe(0);
});
test("eraser activates via E shortcut", async ({ editorPage: page }) => {
await page.keyboard.press("e");
await page.waitForTimeout(300);
await expect(page.locator("[data-tool='eraser']")).toHaveAttribute("data-tool-active", "true");
});
test("eraser options bar shows size and opacity", async ({ editorPage: page }) => {
await selectTool(page, "eraser");
const optionsBar = page.locator('[data-testid="editor-options-bar"]');
await expect(optionsBar.getByText("Size")).toBeVisible();
await expect(optionsBar.getByText("Opacity")).toBeVisible();
});
test("pencil tool activates via N shortcut", async ({ editorPage: page }) => {
await page.keyboard.press("n");
await page.waitForTimeout(300);
await expect(page.locator("[data-tool='pencil']")).toHaveAttribute("data-tool-active", "true");
});
test("pencil options bar hides hardness", async ({ editorPage: page }) => {
await selectTool(page, "pencil");
const optionsBar = page.locator('[data-testid="editor-options-bar"]');
await expect(optionsBar.getByText("Size")).toBeVisible();
await expect(optionsBar.getByText("Opacity")).toBeVisible();
// Pencil is always hard, so hardness should not appear
await expect(optionsBar.getByText("Hardness")).not.toBeVisible();
});
test("shape fill color applies", async ({ editorPage: page }) => {
await selectTool(page, "shape-rect");
// The fill colour is chosen via the "Fill" color picker button in the
// options bar (a ShapeColorPicker, labelled "<label> color picker").
const fillPicker = page.locator("button[aria-label='Fill color picker']");
await expect(fillPicker).toBeVisible();
});
test("shape stroke width control is visible", async ({ editorPage: page }) => {
await selectTool(page, "shape-rect");
const widthLabel = page.locator("label").filter({ hasText: /^Width/ });
await expect(widthLabel).toBeVisible();
const widthSlider = widthLabel.locator("input[type='range']");
await expect(widthSlider).toBeVisible();
});
});