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");
});
});