// Editor QA sweep against the isolated Docker container (auth off). // Exercises 18 feature groups: navigation, welcome screen, layers, history, // drawing tools, text, keyboard shortcuts, colors, filters/adjustments, // fill dialog, layer effects, export, selection tools, transform/resize, // crop, menu bar, rulers/guides, options bar. // Console-error instrumented via qa-helpers.ts. import path from "node:path"; import { expect, type Page, test } from "@playwright/test"; import { instrument, isClean, issuesSummary, type PageIssues } from "./qa-helpers"; // --------------------------------------------------------------------------- // Shared helpers (auth-free for QA container) // --------------------------------------------------------------------------- async function gotoEditor(page: Page): Promise { const issues = instrument(page); await page.goto("/editor", { waitUntil: "domcontentloaded" }); await page.waitForTimeout(2000); return issues; } async function createNewDocument(page: Page, width = 1920, height = 1080): Promise { const newDocBtn = page.getByText("New Document"); if (await newDocBtn.isVisible().catch(() => false)) { await newDocBtn.click(); await page.waitForTimeout(500); await page.locator('button:has-text("Create")').click(); await page.waitForTimeout(2000); } } async function waitForCanvas(page: Page): Promise { await page.locator("canvas").first().waitFor({ state: "visible", timeout: 10_000 }); } async function selectTool(page: Page, toolName: string): Promise { await page.locator(`[data-tool="${toolName}"]`).click(); await page.waitForTimeout(300); } async function drawOnCanvas( page: Page, x1: number, y1: number, x2: number, y2: number, ): Promise { const canvas = page.locator("canvas").first(); const box = await canvas.boundingBox(); if (!box) throw new Error("Canvas not found"); await page.mouse.move(box.x + x1, box.y + y1); await page.mouse.down(); await page.mouse.move(box.x + x2, box.y + y2, { steps: 10 }); await page.mouse.up(); await page.waitForTimeout(300); } async function getCanvasBox(page: Page) { const canvas = page.locator("canvas").first(); const box = await canvas.boundingBox(); if (!box) throw new Error("Canvas not found"); return box; } // --------------------------------------------------------------------------- // 1. Navigation & Layout // --------------------------------------------------------------------------- test.describe("Editor Navigation & Layout", () => { test("editor loads at /editor with no console errors", async ({ page }) => { const issues = await gotoEditor(page); // Welcome screen should be visible (sr-only h1 + visible h2 both match; // target the visible h2 specifically) await expect(page.getByRole("heading", { name: "Image Editor", level: 2 })).toBeVisible(); await expect(page.getByText("Open Image")).toBeVisible(); await expect(page.getByText("New Document")).toBeVisible(); await expect(page.getByText("paste from clipboard")).toBeVisible(); expect(isClean(issues), `Console errors on load:\n${issuesSummary(issues)}`).toBe(true); }); test("sidebar shows Editor link between Automate and Files", async ({ page }) => { const issues = instrument(page); await page.goto("/", { waitUntil: "domcontentloaded" }); await page.waitForTimeout(2000); const sidebarLinks = page.locator("nav a, aside a, [class*='sidebar'] a"); const labels = await sidebarLinks.allTextContents(); const flat = labels.join("|"); expect(flat).toContain("Editor"); // Navigate to editor via link await page.locator('a[href="/editor"]').click(); await page.waitForTimeout(2000); expect(page.url()).toContain("/editor"); expect(isClean(issues), `Console errors:\n${issuesSummary(issues)}`).toBe(true); }); test("editor renders four-zone layout after new document", async ({ page }) => { const issues = await gotoEditor(page); await createNewDocument(page); // Toolbar on the left await expect(page.locator("[data-tool='move']")).toBeVisible(); // Right panel with tabs await expect(page.locator("[data-testid='tab-layers']")).toBeVisible(); await expect(page.locator("[data-testid='tab-adjustments']")).toBeVisible(); await expect(page.locator("[data-testid='tab-history']")).toBeVisible(); // Canvas await expect(page.locator("canvas").first()).toBeVisible(); expect(isClean(issues), `Console errors:\n${issuesSummary(issues)}`).toBe(true); }); test("right panel collapses and expands", async ({ page }) => { const issues = await gotoEditor(page); await expect(page.locator("[data-testid='tab-layers']")).toBeVisible(); // Collapse await page.locator("button[aria-label='Collapse panel']").click(); await page.waitForTimeout(300); await expect(page.locator("[data-testid='tab-layers']")).not.toBeVisible(); // Expand await page.locator("button[aria-label='Expand panel']").click(); await page.waitForTimeout(300); await expect(page.locator("[data-testid='tab-layers']")).toBeVisible(); expect(isClean(issues), `Console errors:\n${issuesSummary(issues)}`).toBe(true); }); test("status bar shows zoom level", async ({ page }) => { await gotoEditor(page); const statusZoom = page.locator("[data-testid='status-zoom']"); await expect(statusZoom).toBeVisible(); await expect(statusZoom.locator("input[type='number']")).toBeVisible(); await expect(statusZoom.locator("text=%")).toBeVisible(); }); }); // --------------------------------------------------------------------------- // 2. Layers Panel // --------------------------------------------------------------------------- test.describe("Editor Layers", () => { test.beforeEach(async ({ page }) => { await gotoEditor(page); await createNewDocument(page); await page.locator("[data-testid='tab-layers']").click(); await page.waitForTimeout(300); }); test("shows default Layer 1", async ({ page }) => { const layersPanel = page.locator("[data-testid='layers-panel']"); await expect(layersPanel).toBeVisible(); await expect(layersPanel.getByText("Layer 1")).toBeVisible(); const layerRows = layersPanel.locator("[role='option']"); await expect(layerRows).toHaveCount(1); }); test("add layer creates new layer", async ({ page }) => { await page.locator("[data-testid='add-layer-btn']").click(); await page.waitForTimeout(300); const layerRows = page.locator("[data-testid='layers-panel'] [role='option']"); await expect(layerRows).toHaveCount(2); }); test("cannot delete last layer", async ({ page }) => { await expect(page.locator("[data-testid='delete-layer-btn']")).toBeDisabled(); }); test("delete enabled with multiple layers", async ({ page }) => { await page.locator("[data-testid='add-layer-btn']").click(); await page.waitForTimeout(300); await expect(page.locator("[data-testid='delete-layer-btn']")).toBeEnabled(); }); test("eye icon toggles visibility", async ({ page }) => { const layerRow = page.locator("[data-testid='layers-panel'] [role='option']").first(); const hideBtn = layerRow.locator("button[aria-label='Hide layer']"); await expect(hideBtn).toBeVisible(); await hideBtn.click(); await page.waitForTimeout(300); await expect(layerRow.locator("button[aria-label='Show layer']")).toBeVisible(); await layerRow.locator("button[aria-label='Show layer']").click(); await page.waitForTimeout(300); await expect(layerRow.locator("button[aria-label='Hide layer']")).toBeVisible(); }); test("lock icon toggles lock state", async ({ page }) => { const layerRow = page.locator("[data-testid='layers-panel'] [role='option']").first(); await layerRow.locator("button[aria-label='Lock layer']").click(); await page.waitForTimeout(300); await expect(layerRow.locator("button[aria-label='Unlock layer']")).toBeVisible(); }); test("blend mode dropdown shows modes", async ({ page }) => { const blendSelect = page.locator("[data-testid='blend-mode-select']"); await expect(blendSelect).toBeVisible(); const texts = await blendSelect.locator("option").allTextContents(); expect(texts).toContain("Normal"); expect(texts).toContain("Multiply"); expect(texts).toContain("Screen"); expect(texts).toContain("Overlay"); }); test("opacity slider works", async ({ page }) => { const slider = page.locator("[data-testid='layer-opacity-slider']"); await expect(slider).toBeVisible(); await expect(slider).toHaveValue("100"); await slider.fill("50"); await page.waitForTimeout(300); await expect(slider).toHaveValue("50"); }); test("double-click layer name enters rename mode", async ({ page }) => { const nameBtn = page .locator("[data-testid='layers-panel'] [role='option']") .first() .locator("button") .filter({ hasText: "Layer 1" }); await nameBtn.dblclick(); await page.waitForTimeout(300); const renameInput = page.locator("[data-testid='layers-panel'] input[type='text']"); await expect(renameInput).toBeVisible(); }); }); // --------------------------------------------------------------------------- // 3. History Panel // --------------------------------------------------------------------------- test.describe("Editor History", () => { test.beforeEach(async ({ page }) => { await gotoEditor(page); await createNewDocument(page); }); test("history tab shows entries and step count", async ({ page }) => { await page.locator("[data-testid='tab-history']").click(); await page.waitForTimeout(300); const entries = page.locator(".flex-1.overflow-y-auto button"); const count = await entries.count(); expect(count).toBeGreaterThanOrEqual(1); await expect(page.getByText(/\d+\s*\/\s*50/)).toBeVisible(); }); test("undo and redo buttons visible", async ({ page }) => { await page.locator("[data-testid='tab-history']").click(); await page.waitForTimeout(300); await expect(page.locator("button[aria-label='Undo']")).toBeVisible(); await expect(page.locator("button[aria-label='Redo']")).toBeVisible(); }); test("drawing creates history entry; undo reverts it", async ({ page }) => { await selectTool(page, "brush"); const canvas = page.locator("canvas").first(); const before = await canvas.screenshot(); await drawOnCanvas(page, 100, 100, 300, 200); await page.waitForTimeout(500); const afterDraw = await canvas.screenshot(); expect(Buffer.compare(before, afterDraw)).not.toBe(0); // Undo await page.keyboard.press("Control+z"); await page.waitForTimeout(500); const afterUndo = await canvas.screenshot(); expect(Buffer.compare(afterDraw, afterUndo)).not.toBe(0); // Redo await page.keyboard.press("Control+Shift+z"); await page.waitForTimeout(500); const afterRedo = await canvas.screenshot(); expect(Buffer.compare(afterUndo, afterRedo)).not.toBe(0); }); }); // --------------------------------------------------------------------------- // 4. Drawing Tools (brush, eraser, pencil, shapes) // --------------------------------------------------------------------------- test.describe("Editor Drawing Tools", () => { test.beforeEach(async ({ page }) => { await gotoEditor(page); await createNewDocument(page); }); test("brush tool activates and draws", async ({ page }) => { const issues = instrument(page); await selectTool(page, "brush"); await expect(page.locator("[data-tool='brush']")).toHaveAttribute("data-tool-active", "true"); // Options bar shows size, opacity, hardness const optionsBar = page.locator(".flex.items-center.h-9"); await expect(optionsBar.getByText("Size")).toBeVisible(); await expect(optionsBar.getByText("Opacity")).toBeVisible(); await expect(optionsBar.getByText("Hardness")).toBeVisible(); // Draw and verify canvas changes 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("eraser tool activates via E shortcut and erases", async ({ page }) => { // Draw first await selectTool(page, "brush"); await drawOnCanvas(page, 100, 100, 300, 200); await page.waitForTimeout(300); // Activate eraser await page.keyboard.press("e"); await page.waitForTimeout(300); await expect(page.locator("[data-tool='eraser']")).toHaveAttribute("data-tool-active", "true"); await expect(page.locator(".flex.items-center.h-9").getByText("Size")).toBeVisible(); }); test("pencil tool activates via N shortcut", async ({ page }) => { await page.keyboard.press("n"); await page.waitForTimeout(300); await expect(page.locator("[data-tool='pencil']")).toHaveAttribute("data-tool-active", "true"); // Pencil hides hardness await expect(page.locator(".flex.items-center.h-9").getByText("Hardness")).not.toBeVisible(); }); test("shape tool draws rectangle on canvas", async ({ 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 fill color and stroke width controls visible", async ({ page }) => { await selectTool(page, "shape-rect"); // ShapeColorPicker renders a label +