import path from "node:path"; import { expect, type Page, test } from "@playwright/test"; const SCREENSHOT_DIR = path.join(__dirname, "screenshots"); let screenshotIndex = 0; async function snap(page: Page, name: string) { screenshotIndex++; const filename = `${String(screenshotIndex).padStart(2, "0")}-${name}.png`; await page.screenshot({ path: path.join(SCREENSHOT_DIR, filename) }); } async function login(page: Page) { await page.goto("/login"); await page.waitForTimeout(2000); await page.fill('input[placeholder*="username" i]', "admin"); await page.fill('input[placeholder*="password" i]', "admin"); await page.click('button:has-text("Login")'); await page.waitForURL("**/", { timeout: 10000 }).catch(() => {}); await page.waitForTimeout(2000); } async function createNewDocument(page: Page) { 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 getCanvasBox(page: Page) { const canvas = page.locator("canvas").first(); const box = await canvas.boundingBox(); if (!box) throw new Error("Canvas not found"); return box; } test.describe("Image Editor - Full GUI Test Suite", () => { test.beforeEach(async ({ page }) => { screenshotIndex = 0; await login(page); }); test("01 - Navigation & Layout", async ({ page }) => { // Check sidebar has Editor item await page.goto("/"); await page.waitForTimeout(2000); const editorLink = page.locator('a:has-text("Editor")'); await expect(editorLink).toBeVisible(); await snap(page, "sidebar-with-editor"); // Navigate to editor await editorLink.click(); await page.waitForTimeout(2000); await expect(page).toHaveURL(/\/editor/); await snap(page, "editor-welcome-screen"); // Verify four-zone layout elements const toolbar = page.locator('[data-tool="move"]'); await expect(toolbar).toBeVisible(); const layersTab = page.locator('[data-testid="tab-layers"]'); await expect(layersTab).toBeVisible(); const adjustmentsTab = page.locator('[data-testid="tab-adjustments"]'); await expect(adjustmentsTab).toBeVisible(); const historyTab = page.locator('[data-testid="tab-history"]'); await expect(historyTab).toBeVisible(); }); test("02 - Welcome Screen & New Document", async ({ page }) => { await page.goto("/editor"); await page.waitForTimeout(2000); // Welcome screen visible (the visible

; an sr-only

shares the text) await expect(page.getByRole("heading", { level: 2, name: "Image Editor" })).toBeVisible(); await expect(page.getByText("Open Image")).toBeVisible(); await expect(page.getByText("New Document")).toBeVisible(); await snap(page, "welcome-screen"); // Open new document dialog await page.getByText("New Document").click(); await page.waitForTimeout(500); await expect(page.getByText("Preset")).toBeVisible(); await snap(page, "new-doc-dialog"); // Change preset to Instagram await page.locator("select").first().selectOption("1080x1080 (Instagram)"); await page.waitForTimeout(300); await snap(page, "new-doc-instagram-preset"); // Select transparent background await page.getByText("Transparent").click(); await page.waitForTimeout(300); await snap(page, "new-doc-transparent"); // Create the document await page.locator('button:has-text("Create")').click(); await page.waitForTimeout(2000); // Canvas should be visible with checkerboard (transparent) const canvas = page.locator("canvas").first(); await expect(canvas).toBeVisible(); await snap(page, "canvas-after-create"); }); test("03 - Brush Tool", async ({ page }) => { await page.goto("/editor"); await page.waitForTimeout(2000); await createNewDocument(page); // Activate brush via toolbar await page.locator('[data-tool="brush"]').click(); await page.waitForTimeout(300); await expect(page.locator('[data-tool-active="true"]')).toHaveAttribute("data-tool", "brush"); await snap(page, "brush-tool-active"); // Verify options bar shows size and opacity await expect(page.getByText("Size", { exact: true }).first()).toBeVisible(); await expect(page.getByText("Opacity", { exact: true }).first()).toBeVisible(); // Draw a brush stroke const box = await getCanvasBox(page); const before = await page.locator("canvas").first().screenshot(); await page.mouse.move(box.x + 100, box.y + 200); await page.mouse.down(); await page.mouse.move(box.x + 500, box.y + 300, { steps: 20 }); await page.mouse.up(); await page.waitForTimeout(500); const after = await page.locator("canvas").first().screenshot(); expect(Buffer.compare(before, after)).not.toBe(0); await snap(page, "brush-stroke-drawn"); // Change brush size via [ and ] keys await page.keyboard.press("]"); await page.keyboard.press("]"); await page.keyboard.press("]"); await page.waitForTimeout(300); await snap(page, "brush-size-increased"); }); test("04 - Eraser Tool", async ({ page }) => { await page.goto("/editor"); await page.waitForTimeout(2000); await createNewDocument(page); // Draw something first with brush await page.locator('[data-tool="brush"]').click(); await page.waitForTimeout(300); const box = await getCanvasBox(page); await page.mouse.move(box.x + 200, box.y + 200); await page.mouse.down(); await page.mouse.move(box.x + 500, box.y + 200, { steps: 15 }); await page.mouse.up(); await page.waitForTimeout(500); // Activate eraser via E key await page.keyboard.press("e"); await page.waitForTimeout(300); await expect(page.locator('[data-tool-active="true"]')).toHaveAttribute("data-tool", "eraser"); await snap(page, "eraser-tool-active"); // Erase over the stroke await page.mouse.move(box.x + 300, box.y + 190); await page.mouse.down(); await page.mouse.move(box.x + 400, box.y + 210, { steps: 10 }); await page.mouse.up(); await page.waitForTimeout(500); await snap(page, "eraser-stroke"); }); test("05 - Shape Tools", async ({ page }) => { await page.goto("/editor"); await page.waitForTimeout(2000); await createNewDocument(page); const box = await getCanvasBox(page); // Rectangle await page.locator('[data-tool="shape-rect"]').click(); await page.waitForTimeout(300); await snap(page, "shape-rect-options"); await page.mouse.move(box.x + 100, box.y + 100); await page.mouse.down(); await page.mouse.move(box.x + 300, box.y + 250, { steps: 10 }); await page.mouse.up(); await page.waitForTimeout(500); await snap(page, "shape-rect-drawn"); // Switch to ellipse via shape dropdown if available const shapeDropdown = page.locator("select").first(); if (await shapeDropdown.isVisible().catch(() => false)) { await shapeDropdown.selectOption("Ellipse"); await page.waitForTimeout(300); } // Draw another shape await page.mouse.move(box.x + 400, box.y + 100); await page.mouse.down(); await page.mouse.move(box.x + 600, box.y + 250, { steps: 10 }); await page.mouse.up(); await page.waitForTimeout(500); await snap(page, "shape-ellipse-drawn"); // Select and move with move tool await page.locator('[data-tool="move"]').click(); await page.waitForTimeout(300); await page.mouse.click(box.x + 200, box.y + 175); await page.waitForTimeout(500); await snap(page, "shape-selected-with-handles"); }); test("06 - Text Tool", async ({ page }) => { await page.goto("/editor"); await page.waitForTimeout(2000); await createNewDocument(page); const box = await getCanvasBox(page); // Activate text tool via T key await page.keyboard.press("t"); await page.waitForTimeout(300); await expect(page.locator('[data-tool-active="true"]')).toHaveAttribute("data-tool", "text"); await snap(page, "text-tool-active"); // Click on canvas to place text await page.mouse.click(box.x + 200, box.y + 200); await page.waitForTimeout(1000); // Type some text (textarea should be active) await page.keyboard.type("Hello SnapOtter!"); await page.waitForTimeout(500); await snap(page, "text-typing"); // Click away to finalize await page.mouse.click(box.x + 50, box.y + 50); await page.waitForTimeout(500); await snap(page, "text-finalized"); }); test("07 - Crop Tool", async ({ page }) => { await page.goto("/editor"); await page.waitForTimeout(2000); await createNewDocument(page); const _box = await getCanvasBox(page); // Activate crop via C key await page.keyboard.press("c"); await page.waitForTimeout(500); await expect(page.locator('[data-tool-active="true"]')).toHaveAttribute("data-tool", "crop"); await snap(page, "crop-tool-active"); // Verify crop options show aspect ratio const _cropOptions = page.getByText(/Free|1:1|4:3|16:9/); await snap(page, "crop-options-bar"); }); test("08 - Layer System", async ({ page }) => { await page.goto("/editor"); await page.waitForTimeout(2000); await createNewDocument(page); // Layers tab should be active by default await expect(page.locator('[data-testid="tab-layers"]')).toBeVisible(); await page.locator('[data-testid="tab-layers"]').click(); await page.waitForTimeout(300); // Should see Layer 1 await expect(page.getByText("Layer 1")).toBeVisible(); await snap(page, "layers-panel-default"); // Add a new layer via + button const _addLayerBtn = page .locator("button") .filter({ has: page.locator("svg") }) .locator("xpath=//button[contains(@class, 'items-center')]") .first(); // Try clicking the + icon at bottom of layers panel const _plusButtons = page .locator('[data-testid="tab-layers"]') .locator("..") .locator("..") .locator("button"); await snap(page, "layers-before-add"); // Use keyboard shortcut to add layer await page.keyboard.press("Control+Shift+n"); await page.waitForTimeout(500); await snap(page, "layers-after-add"); // Check blend mode dropdown const blendDropdown = page.locator("select").filter({ hasText: /Normal/ }); if (await blendDropdown.isVisible().catch(() => false)) { await blendDropdown.click(); await page.waitForTimeout(300); await snap(page, "blend-mode-dropdown"); } // Toggle layer visibility const eyeIcons = page.locator( '[aria-label*="visibility" i], [aria-label*="toggle" i], [title*="visibility" i]', ); if ( await eyeIcons .first() .isVisible() .catch(() => false) ) { await eyeIcons.first().click(); await page.waitForTimeout(300); await snap(page, "layer-visibility-toggled"); } }); test("09 - Color System", async ({ page }) => { await page.goto("/editor"); await page.waitForTimeout(2000); await createNewDocument(page); // Foreground/background swatches should be visible await expect(page.getByText("Foreground")).toBeVisible(); await snap(page, "color-panel-default"); // Click foreground swatch to open picker const _fgSwatch = page.locator('[data-testid="color-foreground"]').or( page .locator("button") .filter({ hasText: /Foreground/ }) .locator("..") .locator("button") .first(), ); // Try clicking the color swatch area const colorArea = page.getByText("Foreground").locator("..").locator("button").first(); if (await colorArea.isVisible().catch(() => false)) { await colorArea.click(); await page.waitForTimeout(500); await snap(page, "color-picker-open"); } // Test D key resets to black/white await page.keyboard.press("d"); await page.waitForTimeout(300); await snap(page, "colors-reset-to-default"); // Test X key swaps colors await page.keyboard.press("x"); await page.waitForTimeout(300); await snap(page, "colors-swapped"); }); test("10 - Adjustments Panel", async ({ page }) => { await page.goto("/editor"); await page.waitForTimeout(2000); await createNewDocument(page); // Switch to Adjustments tab await page.locator('[data-testid="tab-adjustments"]').click(); await page.waitForTimeout(500); await snap(page, "adjustments-panel"); // Verify adjustment sliders present await expect(page.getByText("Brightness", { exact: true })).toBeVisible(); await expect(page.getByText("Contrast", { exact: true })).toBeVisible(); await expect(page.getByText("Saturation", { exact: true })).toBeVisible(); await snap(page, "adjustments-sliders"); // Verify auto buttons await expect(page.getByText("Auto Tone")).toBeVisible(); await expect(page.getByText("Auto Contrast")).toBeVisible(); await snap(page, "auto-buttons"); // Scroll to see levels section const panel = page .locator('[data-testid="tab-adjustments"]') .locator("..") .locator("..") .locator("div.overflow-y-auto"); if (await panel.isVisible().catch(() => false)) { await panel.evaluate((el) => (el.scrollTop = 400)); await page.waitForTimeout(500); await snap(page, "levels-section"); await panel.evaluate((el) => (el.scrollTop = 800)); await page.waitForTimeout(500); await snap(page, "curves-section"); await panel.evaluate((el) => (el.scrollTop = 1200)); await page.waitForTimeout(500); await snap(page, "filters-section"); } }); test("11 - History & Undo/Redo", async ({ page }) => { await page.goto("/editor"); await page.waitForTimeout(2000); await createNewDocument(page); const box = await getCanvasBox(page); // Draw a shape await page.locator('[data-tool="shape-rect"]').click(); await page.waitForTimeout(300); await page.mouse.move(box.x + 100, box.y + 100); await page.mouse.down(); await page.mouse.move(box.x + 300, box.y + 250, { steps: 10 }); await page.mouse.up(); await page.waitForTimeout(500); // Draw a brush stroke await page.keyboard.press("b"); await page.waitForTimeout(300); await page.mouse.move(box.x + 400, box.y + 150); await page.mouse.down(); await page.mouse.move(box.x + 600, box.y + 300, { steps: 15 }); await page.mouse.up(); await page.waitForTimeout(500); // Open history panel await page.locator('[data-testid="tab-history"]').click(); await page.waitForTimeout(500); await snap(page, "history-with-actions"); // Verify history entries exist const _historyEntries = page .locator('[data-testid="tab-history"]') .locator("..") .locator("..") .locator("div.overflow-y-auto"); await snap(page, "history-panel-entries"); // Undo with Ctrl+Z const beforeUndo = await page.locator("canvas").first().screenshot(); await page.keyboard.press("Control+z"); await page.waitForTimeout(500); const afterUndo = await page.locator("canvas").first().screenshot(); expect(Buffer.compare(beforeUndo, afterUndo)).not.toBe(0); await snap(page, "after-undo"); // Redo with Ctrl+Shift+Z await page.keyboard.press("Control+Shift+z"); await page.waitForTimeout(500); await snap(page, "after-redo"); }); test("12 - Keyboard Shortcuts", async ({ page }) => { await page.goto("/editor"); await page.waitForTimeout(2000); await createNewDocument(page); const toolShortcuts: [string, string][] = [ ["v", "move"], ["b", "brush"], ["e", "eraser"], ["u", "shape-rect"], ["t", "text"], ["c", "crop"], ["i", "eyedropper"], ["h", "hand"], ["z", "zoom"], ["m", "marquee-rect"], ["g", "fill"], ]; for (const [key, _expectedTool] of toolShortcuts) { await page.keyboard.press(key); await page.waitForTimeout(200); const activeTool = page.locator('[data-tool-active="true"]'); const toolAttr = await activeTool.getAttribute("data-tool"); // Just verify the active tool changed (some tools may have different sub-types) expect(toolAttr).toBeTruthy(); } await snap(page, "keyboard-shortcuts-tested"); // Test D resets colors await page.keyboard.press("d"); await page.waitForTimeout(200); // Test X swaps await page.keyboard.press("x"); await page.waitForTimeout(200); await snap(page, "color-shortcuts-tested"); }); test("13 - Right Panel Toggle", async ({ page }) => { await page.goto("/editor"); await page.waitForTimeout(2000); await createNewDocument(page); // Panel should be visible await expect(page.locator('[data-testid="tab-layers"]')).toBeVisible(); await snap(page, "panel-visible"); // Press Tab to toggle panel await page.keyboard.press("Tab"); await page.waitForTimeout(500); await snap(page, "panel-collapsed"); // Press Tab again to restore await page.keyboard.press("Tab"); await page.waitForTimeout(500); await snap(page, "panel-restored"); }); test("14 - Multiple Shapes & Move", async ({ page }) => { await page.goto("/editor"); await page.waitForTimeout(2000); await createNewDocument(page); const box = await getCanvasBox(page); // Draw 3 shapes await page.locator('[data-tool="shape-rect"]').click(); await page.waitForTimeout(300); // Shape 1 await page.mouse.move(box.x + 50, box.y + 50); await page.mouse.down(); await page.mouse.move(box.x + 200, box.y + 150, { steps: 5 }); await page.mouse.up(); await page.waitForTimeout(300); // Shape 2 await page.mouse.move(box.x + 250, box.y + 50); await page.mouse.down(); await page.mouse.move(box.x + 400, box.y + 150, { steps: 5 }); await page.mouse.up(); await page.waitForTimeout(300); // Shape 3 await page.mouse.move(box.x + 450, box.y + 50); await page.mouse.down(); await page.mouse.move(box.x + 600, box.y + 150, { steps: 5 }); await page.mouse.up(); await page.waitForTimeout(300); await snap(page, "three-shapes-drawn"); // Switch to move tool and select shape await page.keyboard.press("v"); await page.waitForTimeout(300); await page.mouse.click(box.x + 125, box.y + 100); await page.waitForTimeout(500); await snap(page, "shape-selected"); // Drag it await page.mouse.move(box.x + 125, box.y + 100); await page.mouse.down(); await page.mouse.move(box.x + 125, box.y + 300, { steps: 10 }); await page.mouse.up(); await page.waitForTimeout(500); await snap(page, "shape-moved"); // Delete with Delete key await page.mouse.click(box.x + 325, box.y + 100); await page.waitForTimeout(300); await page.keyboard.press("Delete"); await page.waitForTimeout(500); await snap(page, "shape-deleted"); }); test("15 - Export Dialog", async ({ page }) => { await page.goto("/editor"); await page.waitForTimeout(2000); await createNewDocument(page); const box = await getCanvasBox(page); // Draw something await page.locator('[data-tool="shape-rect"]').click(); await page.waitForTimeout(300); await page.mouse.move(box.x + 200, box.y + 150); await page.mouse.down(); await page.mouse.move(box.x + 500, box.y + 350, { steps: 10 }); await page.mouse.up(); await page.waitForTimeout(500); // Open export dialog via Ctrl+Shift+S await page.keyboard.press("Control+Shift+s"); await page.waitForTimeout(1000); await snap(page, "export-dialog"); // Check format options const pngBtn = page.getByText("PNG"); const jpegBtn = page.getByText("JPEG"); const webpBtn = page.getByText("WebP"); if (await pngBtn.isVisible().catch(() => false)) { await expect(pngBtn).toBeVisible(); await expect(jpegBtn).toBeVisible(); await expect(webpBtn).toBeVisible(); // Select JPEG to see quality slider await jpegBtn.click(); await page.waitForTimeout(300); await snap(page, "export-jpeg-quality"); } }); test("16 - Clone Stamp Tool", async ({ page }) => { await page.goto("/editor"); await page.waitForTimeout(2000); await createNewDocument(page); // Activate clone stamp via S key await page.keyboard.press("s"); await page.waitForTimeout(300); await expect(page.locator('[data-tool-active="true"]')).toHaveAttribute( "data-tool", "clone-stamp", ); await snap(page, "clone-stamp-active"); // Verify options bar await expect(page.getByText("Size")).toBeVisible(); }); test("17 - Dodge/Burn/Sponge Tool", async ({ page }) => { await page.goto("/editor"); await page.waitForTimeout(2000); await createNewDocument(page); // Activate dodge via O key await page.keyboard.press("o"); await page.waitForTimeout(300); await snap(page, "dodge-tool-active"); // Check options show range and exposure const _optionsBar = page .locator("div") .filter({ hasText: /Range|Exposure|Dodge|Burn|Sponge/ }) .first(); await snap(page, "dodge-burn-options"); }); test("18 - Selection Tools", async ({ page }) => { await page.goto("/editor"); await page.waitForTimeout(2000); await createNewDocument(page); const box = await getCanvasBox(page); // Marquee selection via M key await page.keyboard.press("m"); await page.waitForTimeout(300); await snap(page, "marquee-tool-active"); // Draw a selection await page.mouse.move(box.x + 100, box.y + 100); await page.mouse.down(); await page.mouse.move(box.x + 400, box.y + 300, { steps: 10 }); await page.mouse.up(); await page.waitForTimeout(500); await snap(page, "marquee-selection-drawn"); // Lasso via L key await page.keyboard.press("l"); await page.waitForTimeout(300); await snap(page, "lasso-tool-active"); // Magic wand via W key await page.keyboard.press("w"); await page.waitForTimeout(300); await snap(page, "magic-wand-active"); }); test("19 - Gradient & Fill Tools", async ({ page }) => { await page.goto("/editor"); await page.waitForTimeout(2000); await createNewDocument(page); // Fill tool via G key await page.keyboard.press("g"); await page.waitForTimeout(300); await snap(page, "fill-tool-active"); // Check tolerance option await expect(page.getByText("Tolerance")).toBeVisible(); await snap(page, "fill-options"); }); test("20 - Navigator Panel", async ({ page }) => { await page.goto("/editor"); await page.waitForTimeout(2000); await createNewDocument(page); // Navigator should show minimap await page.waitForTimeout(1000); await snap(page, "navigator-minimap"); }); test("21 - Status Bar", async ({ page }) => { await page.goto("/editor"); await page.waitForTimeout(2000); await createNewDocument(page); // Verify zoom percentage const zoomInput = page.locator('[data-testid="status-zoom"] input'); if (await zoomInput.isVisible().catch(() => false)) { const zoomValue = await zoomInput.inputValue(); expect(Number(zoomValue)).toBeGreaterThan(0); } // Verify dimensions const dimensions = page.locator('[data-testid="status-dimensions"]'); if (await dimensions.isVisible().catch(() => false)) { const text = await dimensions.textContent(); expect(text).toContain("1920"); expect(text).toContain("1080"); } await snap(page, "status-bar"); }); });