mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
A release-readiness QA pass over the whole product. The commits split into defects a user would hit and gates that were reporting green while measuring nothing. ## Fixes that change behaviour Rate limiting was bypassable on every install: TRUST_PROXY defaulted to true, so request.ip came from a client-set header and a forged X-Forwarded-For got past the login limiter. The default is now a private-network trust list. A transient Postgres outage stranded in-flight jobs, leaving finished output on disk with no row pointing at it. A reconciler now resolves those rows and adopts the bytes rather than dropping the work. A Redis connection that moved to a new address wedged every read-blocked consumer, so completions stopped signalling while health still answered 200. Socket timeouts plus subscriber pings recover it. Installing more than one AI bundle left the shared venv multi-versioned and silently broke three tools. The installer now reconciles distributions to one version each. Converting an image to JXL at quality 1 through 4 returned a 500, because libjxl 0.7 rejects the distance those values compute. The quality is floored at what the encoder honours. A missing ffmpeg was also reported to the user as a corrupt upload; it now says the engine is unavailable. RAW uploads reached an unpatched LibRaw on arm64, so it is built from source at 0.22.2, and the release scan was split so it can fail on an unfixed critical instead of hiding it behind ignore-unfixed. ## Gates that could not fail Two mutation lanes ran zero mutants because Stryker crawled the gitignored docs build; coverage discarded its whole report on any failing test; the lint gate skipped root tests, scripts, and two workspaces; and several generated matrices counted a host missing ffmpeg as a passing tool. Each now measures what it claims. Full evidence and the outstanding release items are tracked locally and are not part of this branch.
712 lines
23 KiB
TypeScript
712 lines
23 KiB
TypeScript
import { expect, type Page, test } from "@playwright/test";
|
|
|
|
let screenshotIndex = 0;
|
|
let screenshotOutputPath: (filename: string) => string;
|
|
|
|
async function snap(page: Page, name: string) {
|
|
screenshotIndex++;
|
|
const filename = `${String(screenshotIndex).padStart(2, "0")}-${name}.png`;
|
|
await page.screenshot({ path: screenshotOutputPath(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 }, testInfo) => {
|
|
screenshotIndex = 0;
|
|
screenshotOutputPath = (filename) => testInfo.outputPath(filename);
|
|
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 <h2>; an sr-only <h1> 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");
|
|
});
|
|
});
|