test: expand coverage across all layers -- 1,268 new tests, fix replace-color div-by-zero

14-agent parallel test expansion covering integration, unit, E2E, E2E-Docker,
cross-format matrix, adversarial, GUI navigation/tools/settings/visual/a11y/perf.

- Integration: expand 23 tool test files with HEIC, stress, batch, edge cases
- Unit: close coverage gaps in image-engine, stores, lib (metadata, auto-enhance,
  connection-store, lazy-with-retry, collage/file-store HEIC preview)
- AI bridge: 141 new tests for dispatcher buffering, crash recovery, OOM/segfault
- Cross-format: 794 parameterized tests (16 formats x 12 tools + no-crash matrix)
- Adversarial: memory stress (50x large file), zero-byte, corrupted headers, unicode
- E2E-Docker: expand 8 spec files with dimension verification, pipeline chains
- GUI E2E: tool UI settings/interactions for all 47 tools, remove all test.skip,
  RBAC per-role verification, visual screenshot naming, cross-browser smoke tests,
  a11y ARIA/focus/contrast, performance budgets, 15-tool stability test
- Fix: replace-color.ts tolerance=0 caused division-by-zero producing NaN pixels

Total: 8,958 tests passing across 202 files. Zero failures, zero skips.
This commit is contained in:
SnapOtter
2026-05-09 18:02:58 +08:00
parent 7b1f09f5d0
commit a0556772e8
76 changed files with 13112 additions and 117 deletions
+192 -4
View File
@@ -115,6 +115,12 @@ test.describe("Login Page", () => {
await page.getByLabel("Password").fill("admin");
await expect(loginBtn).toBeDisabled();
});
test("SnapOtter branding is visible on login page", async ({ page }) => {
await page.goto("/login");
await expect(page.getByText("SnapOtter").first()).toBeVisible();
});
});
// ---------------------------------------------------------------------------
@@ -132,6 +138,21 @@ test.describe("Home Page - Before Upload", () => {
await expect(page.getByText("Essentials").first()).toBeVisible();
});
test("each tool category header is visible in tool panel", async ({ loggedInPage: page }) => {
const toolPanel = page.locator("aside, [class*='tool-panel'], section").filter({
hasText: "Essentials",
});
await expect(toolPanel.getByText("Essentials").first()).toBeVisible();
await expect(toolPanel.getByText("Optimization").first()).toBeVisible();
});
test("clicking a tool in the panel navigates to its page", async ({ loggedInPage: page }) => {
// Click on a specific tool from the panel
const compressLink = page.getByText("Compress").first();
await compressLink.click();
await expect(page).toHaveURL("/compress");
});
test("search filters tools in tool panel", async ({ loggedInPage: page }) => {
const searchInput = page.getByPlaceholder(/search/i).first();
await searchInput.fill("resize");
@@ -195,6 +216,22 @@ test.describe("Home Page - After Upload", () => {
await expect(img).toBeVisible();
});
test("Change file button resets to dropzone state", async ({ loggedInPage: page }) => {
await uploadTestImage(page);
// File info should be visible
await expect(page.getByText(/test-image/i).first()).toBeVisible();
// Click Change file
await page.getByText("Change file").click();
await page.waitForTimeout(300);
// Dropzone should reappear
const dropzone = page.locator("[class*='border-dashed']").first();
await expect(dropzone).toBeVisible();
await expect(page.getByText("Upload from computer")).toBeVisible();
});
test("multi-upload shows file count badge", async ({ loggedInPage: page }) => {
// Upload first image
await uploadTestImage(page);
@@ -239,6 +276,11 @@ test.describe("Fullscreen Grid Page", () => {
await expect(page.getByText("Essentials")).toBeVisible();
await expect(page.getByText("Optimization")).toBeVisible();
await expect(page.getByText("Adjustments")).toBeVisible();
await expect(page.getByText("Watermark & Overlay")).toBeVisible();
await expect(page.getByText("Utilities")).toBeVisible();
await expect(page.getByText("Layout & Composition")).toBeVisible();
await expect(page.getByText("Format & Conversion")).toBeVisible();
await expect(page.getByText("AI Tools")).toBeVisible();
});
test("tool cards are links to tool pages", async ({ loggedInPage: page }) => {
@@ -341,16 +383,61 @@ test.describe("Tool Page - Resize", () => {
// Tool Page - Parameterized structure tests across multiple tools
// ---------------------------------------------------------------------------
const DROPZONE_TOOLS = [
// Essentials
{ id: "resize", name: "Resize" },
{ id: "crop", name: "Crop" },
{ id: "rotate", name: "Rotate" },
{ id: "convert", name: "Convert" },
{ id: "compress", name: "Compress" },
{ id: "adjust-colors", name: "Adjust Colors" },
{ id: "watermark-text", name: "Text Watermark" },
{ id: "border", name: "Border" },
// Optimization
{ id: "optimize-for-web", name: "Optimize for Web" },
{ id: "strip-metadata", name: "Remove Metadata" },
{ id: "edit-metadata", name: "Edit Metadata" },
{ id: "bulk-rename", name: "Bulk Rename" },
{ id: "image-to-pdf", name: "Image to PDF" },
{ id: "favicon", name: "Favicon Generator" },
// Adjustments
{ id: "adjust-colors", name: "Adjust Colors" },
{ id: "sharpening", name: "Sharpening" },
{ id: "replace-color", name: "Replace & Invert Color" },
{ id: "color-blindness", name: "Color Blindness Simulation" },
// AI Tools
{ id: "remove-background", name: "Remove Background" },
{ id: "upscale", name: "Image Upscaling" },
{ id: "erase-object", name: "Object Eraser" },
{ id: "ocr", name: "OCR / Text Extraction" },
{ id: "blur-faces", name: "Face / PII Blur" },
{ id: "smart-crop", name: "Smart Crop" },
{ id: "image-enhancement", name: "Image Enhancement" },
{ id: "enhance-faces", name: "Face Enhancement" },
{ id: "colorize", name: "AI Colorization" },
{ id: "noise-removal", name: "Noise Removal" },
{ id: "red-eye-removal", name: "Red Eye Removal" },
{ id: "restore-photo", name: "Photo Restoration" },
{ id: "passport-photo", name: "Passport Photo" },
{ id: "content-aware-resize", name: "Content-Aware Resize" },
{ id: "transparency-fixer", name: "PNG Transparency Fixer" },
// Watermark & Overlay
{ id: "watermark-text", name: "Text Watermark" },
{ id: "watermark-image", name: "Image Watermark" },
{ id: "text-overlay", name: "Text Overlay" },
{ id: "compose", name: "Image Composition" },
// Utilities
{ id: "info", name: "Image Info" },
{ id: "compare", name: "Image Compare" },
{ id: "find-duplicates", name: "Find Duplicates" },
{ id: "color-palette", name: "Color Palette" },
{ id: "barcode-read", name: "Barcode Reader" },
{ id: "image-to-base64", name: "Image to Base64" },
// Layout & Composition
{ id: "stitch", name: "Stitch / Combine" },
{ id: "split", name: "Image Splitting" },
{ id: "border", name: "Border & Frame" },
{ id: "beautify", name: "Beautify Screenshot" },
// Format & Conversion
{ id: "svg-to-raster", name: "SVG to Raster" },
{ id: "vectorize", name: "Image to SVG" },
{ id: "gif-tools", name: "GIF Tools" },
];
const NO_DROPZONE_TOOLS = [
@@ -416,6 +503,21 @@ test.describe("Tool Page - Settings and Process Flow", () => {
await expect(page.getByText("Settings").first()).toBeVisible();
});
test("resize: download link appears after processing", async ({ loggedInPage: page }) => {
await page.goto("/resize");
await uploadTestImage(page);
// Click the process button
const processBtn = page.getByRole("button", { name: /process/i }).first();
await expect(processBtn).toBeVisible();
await processBtn.click();
// Wait for processing to complete and download link to appear
await expect(page.getByRole("link", { name: /download/i }).first()).toBeVisible({
timeout: 15_000,
});
});
test("mobile: settings panel is collapsible on tool page", async ({ browser }) => {
const context = await browser.newContext({
viewport: { width: 375, height: 667 },
@@ -504,6 +606,25 @@ test.describe("Automate Page", () => {
await expect(page.getByText("No steps yet")).not.toBeVisible();
});
test("Save button appears when pipeline has steps", async ({ loggedInPage: page }) => {
await page.goto("/automate");
// Save button should not be visible before adding steps
await expect(page.getByRole("button", { name: /^Save$/i })).not.toBeVisible();
// Add a tool step
const resizeTool = page.locator("[data-tool-id='resize']").first();
if (await resizeTool.isVisible({ timeout: 3000 }).catch(() => false)) {
await resizeTool.click();
} else {
await page.getByText("Resize").first().click();
}
await page.waitForTimeout(500);
// Save button should now be visible
await expect(page.getByRole("button", { name: /^Save$/i })).toBeVisible();
});
test("process button remains disabled with steps but no file", async ({ loggedInPage: page }) => {
await page.goto("/automate");
@@ -540,13 +661,16 @@ test.describe("Files Page", () => {
// Sidebar Navigation
// ---------------------------------------------------------------------------
test.describe("Sidebar Navigation", () => {
test("sidebar has 4 top items: Tools, Grid, Automate, Files", async ({ loggedInPage: page }) => {
test("sidebar has 5 top items: Tools, Grid, Automate, Editor, Files", async ({
loggedInPage: page,
}) => {
const sidebar = page.locator("aside");
await expect(sidebar).toBeVisible();
await expect(sidebar.getByText("Tools")).toBeVisible();
await expect(sidebar.getByText("Grid")).toBeVisible();
await expect(sidebar.getByText("Automate")).toBeVisible();
await expect(sidebar.getByText("Editor")).toBeVisible();
await expect(sidebar.getByText("Files")).toBeVisible();
});
@@ -578,6 +702,11 @@ test.describe("Sidebar Navigation", () => {
await expect(page).toHaveURL("/automate");
});
test("Editor link navigates to /editor", async ({ loggedInPage: page }) => {
await page.locator("aside").getByText("Editor").click();
await expect(page).toHaveURL("/editor");
});
test("Files link navigates to /files", async ({ loggedInPage: page }) => {
await page.locator("aside").getByText("Files").click();
await expect(page).toHaveURL("/files");
@@ -794,6 +923,15 @@ test.describe("Routing Edge Cases", () => {
await expect(page).toHaveURL("/adjust-colors");
});
test("/login when already logged in redirects to /", async ({ loggedInPage: page }) => {
// loggedInPage already has authentication via storageState
await page.goto("/login");
// Should redirect away from login since already authenticated
await page.waitForURL("/", { timeout: 10_000 });
await expect(page).toHaveURL("/");
});
});
// ---------------------------------------------------------------------------
@@ -907,6 +1045,36 @@ test.describe("Mobile Navigation", () => {
await context.close();
});
test("tapping backdrop closes sidebar overlay", async ({ browser }) => {
const context = await browser.newContext({
viewport: { width: 375, height: 667 },
});
const page = await context.newPage();
await page.goto("/login");
await page.getByLabel("Username").fill("admin");
await page.getByLabel("Password").fill("admin");
await page.getByRole("button", { name: /login/i }).click();
await page.waitForURL("/", { timeout: 15_000 });
// Open hamburger menu
const topBar = page.locator(".fixed").filter({ hasText: "SnapOtter" }).first();
const hamburger = topBar.locator("button").first();
await hamburger.click();
// Sidebar overlay should appear
const backdrop = page.locator(".fixed.inset-0").first();
await expect(backdrop).toBeVisible();
// Click the backdrop to close
await backdrop.click({ position: { x: 300, y: 300 } });
await page.waitForTimeout(300);
// Sidebar overlay should be closed (expanded sidebar nav no longer visible)
await expect(backdrop).not.toBeVisible();
await context.close();
});
test("bottom nav navigates between all main sections", async ({ browser }) => {
const context = await browser.newContext({
viewport: { width: 375, height: 667 },
@@ -934,4 +1102,24 @@ test.describe("Mobile Navigation", () => {
await context.close();
});
test("bottom nav has Editor link", async ({ browser }) => {
const context = await browser.newContext({
viewport: { width: 375, height: 667 },
});
const page = await context.newPage();
await page.goto("/login");
await page.getByLabel("Username").fill("admin");
await page.getByLabel("Password").fill("admin");
await page.getByRole("button", { name: /login/i }).click();
await page.waitForURL("/", { timeout: 15_000 });
const bottomNav = page.locator("nav.fixed");
await expect(bottomNav.getByText("Editor")).toBeVisible();
await bottomNav.getByText("Editor").click();
await expect(page).toHaveURL("/editor");
await context.close();
});
});