Files
SnapOtter/tests/e2e/gui-batch.spec.ts
T
SnapOtter 03f82567d0 test: expand API and GUI test coverage across all tools
Add ~500 new E2E tests and ~300 new integration tests covering:

- 24 new GUI E2E specs: navigation, responsive layout, keyboard shortcuts,
  tool UI for all 35 non-AI tools, batch/pipeline workflows, settings/RBAC,
  visual regression, accessibility, and performance budgets
- 3 new E2E-Docker specs: batch workflows, advanced pipelines, cross-format
- 1 new adversarial integration test: memory pressure, corrupted files,
  unicode filenames, extreme dimensions, pipeline/batch edge cases
- 29 expanded integration test files: HEIC/HEIF input, large files, parameter
  boundaries, batch processing, format edge cases across all tools
- Cross-format matrix expanded: 641 tests covering every tool x 18 formats
- AI bridge unit tests expanded: lifecycle, tool modules, error propagation
- Unit test gaps filled: analytics, tool-registry, web stores

Also fixes:
- vitest.config.ts: exclude e2e-docs and e2e-landing from Vitest runner
- AI E2E specs: add sidecar health check to skip gracefully when Python
  AI backend is not running instead of timing out
2026-04-29 01:39:25 +08:00

385 lines
14 KiB
TypeScript

import path from "node:path";
import { expect, test, waitForProcessing } from "./helpers";
// ---------------------------------------------------------------------------
// Helper: resolve fixture image paths
// ---------------------------------------------------------------------------
function getFixturePath(name: string): string {
return path.join(process.cwd(), "tests", "fixtures", name);
}
const FIXTURE_JPG = getFixturePath("test-100x100.jpg");
const FIXTURE_PNG = getFixturePath("test-200x150.png");
const FIXTURE_WEBP = getFixturePath("test-50x50.webp");
// ---------------------------------------------------------------------------
// Multi-file upload tests
// ---------------------------------------------------------------------------
test.describe("Multi-file upload", () => {
test("upload 2 files via file chooser and both appear in Files section", async ({
loggedInPage: page,
}) => {
await page.goto("/resize");
const fileChooserPromise = page.waitForEvent("filechooser");
const dropzone = page.locator("[class*='border-dashed']").first();
await dropzone.click();
const fileChooser = await fileChooserPromise;
await fileChooser.setFiles([FIXTURE_JPG, FIXTURE_PNG]);
await page.waitForTimeout(1000);
// Both files should be registered
await expect(page.getByText("Files (2)")).toBeVisible();
});
test("upload 3+ files and all are listed with filenames and sizes", async ({
loggedInPage: page,
}) => {
await page.goto("/resize");
const fileChooserPromise = page.waitForEvent("filechooser");
const dropzone = page.locator("[class*='border-dashed']").first();
await dropzone.click();
const fileChooser = await fileChooserPromise;
await fileChooser.setFiles([FIXTURE_JPG, FIXTURE_PNG, FIXTURE_WEBP]);
await page.waitForTimeout(1000);
// All 3 files should be registered
await expect(page.getByText("Files (3)")).toBeVisible();
// The currently selected file info should show a filename and size
await expect(page.getByText(/test-/i).first()).toBeVisible();
await expect(page.getByText(/KB|B/i).first()).toBeVisible();
});
test("'+ Add more' adds files to existing set", async ({ loggedInPage: page }) => {
await page.goto("/resize");
// Upload initial file
const fileChooserPromise = page.waitForEvent("filechooser");
const dropzone = page.locator("[class*='border-dashed']").first();
await dropzone.click();
const fileChooser = await fileChooserPromise;
await fileChooser.setFiles([FIXTURE_JPG]);
await page.waitForTimeout(500);
await expect(page.getByText("Files (1)")).toBeVisible();
// Click "+ Add more" which triggers a programmatic file input
const addMorePromise = page.waitForEvent("filechooser");
await page.getByText("+ Add more").click();
const addMoreChooser = await addMorePromise;
await addMoreChooser.setFiles([FIXTURE_PNG, FIXTURE_WEBP]);
await page.waitForTimeout(500);
// Now should show 3 files
await expect(page.getByText("Files (3)")).toBeVisible();
});
test("'Clear all' removes all files and returns to dropzone", async ({ loggedInPage: page }) => {
await page.goto("/resize");
// Upload files
const fileChooserPromise = page.waitForEvent("filechooser");
const dropzone = page.locator("[class*='border-dashed']").first();
await dropzone.click();
const fileChooser = await fileChooserPromise;
await fileChooser.setFiles([FIXTURE_JPG, FIXTURE_PNG]);
await page.waitForTimeout(500);
await expect(page.getByText("Files (2)")).toBeVisible();
// Clear all files
await page.getByText("Clear all").click();
// Dropzone should reappear
await expect(page.getByText("Upload from computer")).toBeVisible();
});
test("ThumbnailStrip shows at bottom with clickable thumbnails", async ({
loggedInPage: page,
}) => {
await page.goto("/resize");
const fileChooserPromise = page.waitForEvent("filechooser");
const dropzone = page.locator("[class*='border-dashed']").first();
await dropzone.click();
const fileChooser = await fileChooserPromise;
await fileChooser.setFiles([FIXTURE_JPG, FIXTURE_PNG, FIXTURE_WEBP]);
await page.waitForTimeout(1000);
// ThumbnailStrip renders when entries.length > 1
// Each thumbnail is a <button> with a title matching the filename
const jpgThumb = page.locator("button[title='test-100x100.jpg']");
const pngThumb = page.locator("button[title='test-200x150.png']");
await expect(jpgThumb).toBeVisible();
await expect(pngThumb).toBeVisible();
// Click the second thumbnail and verify it becomes selected (outline-primary)
await pngThumb.click();
await expect(pngThumb).toHaveClass(/outline-primary/);
});
test("Previous/Next arrows cycle through images", async ({ loggedInPage: page }) => {
await page.goto("/resize");
const fileChooserPromise = page.waitForEvent("filechooser");
const dropzone = page.locator("[class*='border-dashed']").first();
await dropzone.click();
const fileChooser = await fileChooserPromise;
await fileChooser.setFiles([FIXTURE_JPG, FIXTURE_PNG, FIXTURE_WEBP]);
await page.waitForTimeout(1000);
// Counter badge should show "1 / 3"
await expect(page.getByText("1 / 3")).toBeVisible();
// Previous arrow should NOT be visible on first image
await expect(page.getByRole("button", { name: "Previous image" })).not.toBeVisible();
// Next arrow should be visible
await expect(page.getByRole("button", { name: "Next image" })).toBeVisible();
// Click next to go to image 2
await page.getByRole("button", { name: "Next image" }).click();
await expect(page.getByText("2 / 3")).toBeVisible();
// Both arrows should be visible on middle image
await expect(page.getByRole("button", { name: "Previous image" })).toBeVisible();
await expect(page.getByRole("button", { name: "Next image" })).toBeVisible();
// Click next to go to image 3
await page.getByRole("button", { name: "Next image" }).click();
await expect(page.getByText("3 / 3")).toBeVisible();
// Next arrow should NOT be visible on last image
await expect(page.getByRole("button", { name: "Next image" })).not.toBeVisible();
// Click previous to go back
await page.getByRole("button", { name: "Previous image" }).click();
await expect(page.getByText("2 / 3")).toBeVisible();
});
test("counter badge shows N/M format", async ({ loggedInPage: page }) => {
await page.goto("/compress");
const fileChooserPromise = page.waitForEvent("filechooser");
const dropzone = page.locator("[class*='border-dashed']").first();
await dropzone.click();
const fileChooser = await fileChooserPromise;
await fileChooser.setFiles([FIXTURE_JPG, FIXTURE_PNG]);
await page.waitForTimeout(1000);
// Should display "1 / 2" counter badge
await expect(page.getByText("1 / 2")).toBeVisible();
});
});
// ---------------------------------------------------------------------------
// Batch processing tests
// ---------------------------------------------------------------------------
test.describe("Batch processing", () => {
test("upload 3 images, process resize, all 3 results available", async ({
loggedInPage: page,
}) => {
await page.goto("/resize");
// Upload 3 images
const fileChooserPromise = page.waitForEvent("filechooser");
const dropzone = page.locator("[class*='border-dashed']").first();
await dropzone.click();
const fileChooser = await fileChooserPromise;
await fileChooser.setFiles([FIXTURE_JPG, FIXTURE_PNG, FIXTURE_WEBP]);
await page.waitForTimeout(1000);
await expect(page.getByText("Files (3)")).toBeVisible();
// Set resize width to 50px
await page.locator("input[placeholder='Auto']").first().fill("50");
// Click the batch process button (should mention file count)
const processBtn = page.getByRole("button", { name: /resize.*3 files/i });
await processBtn.click();
// Wait for processing to complete
await waitForProcessing(page, 30_000);
// After processing, the image area should show a result
await expect(page.locator("section[aria-label='Image area'] img").first()).toBeVisible({
timeout: 15_000,
});
});
test("'Download All' button visible for batch results", async ({ loggedInPage: page }) => {
await page.goto("/resize");
// Upload multiple images
const fileChooserPromise = page.waitForEvent("filechooser");
const dropzone = page.locator("[class*='border-dashed']").first();
await dropzone.click();
const fileChooser = await fileChooserPromise;
await fileChooser.setFiles([FIXTURE_JPG, FIXTURE_PNG]);
await page.waitForTimeout(1000);
// Set resize width
await page.locator("input[placeholder='Auto']").first().fill("50");
// Process batch
await page.getByRole("button", { name: /resize.*2 files/i }).click();
await waitForProcessing(page, 30_000);
// Wait for at least one result image
await expect(page.locator("section[aria-label='Image area'] img").first()).toBeVisible({
timeout: 15_000,
});
// Download All (ZIP) button should be visible
await expect(page.getByRole("button", { name: /download all/i })).toBeVisible();
});
test("navigate through batch results with Previous/Next", async ({ loggedInPage: page }) => {
await page.goto("/resize");
// Upload 2 images
const fileChooserPromise = page.waitForEvent("filechooser");
const dropzone = page.locator("[class*='border-dashed']").first();
await dropzone.click();
const fileChooser = await fileChooserPromise;
await fileChooser.setFiles([FIXTURE_JPG, FIXTURE_PNG]);
await page.waitForTimeout(1000);
// Set resize width
await page.locator("input[placeholder='Auto']").first().fill("50");
// Process batch
await page.getByRole("button", { name: /resize.*2 files/i }).click();
await waitForProcessing(page, 30_000);
// Wait for result
await expect(page.locator("section[aria-label='Image area'] img").first()).toBeVisible({
timeout: 15_000,
});
// Should show counter "1 / 2"
await expect(page.getByText("1 / 2")).toBeVisible();
// Navigate to next result
await page.getByRole("button", { name: "Next image" }).click();
await expect(page.getByText("2 / 2")).toBeVisible();
// Navigate back
await page.getByRole("button", { name: "Previous image" }).click();
await expect(page.getByText("1 / 2")).toBeVisible();
});
test("each batch result has download link", async ({ loggedInPage: page }) => {
await page.goto("/resize");
// Upload 2 images
const fileChooserPromise = page.waitForEvent("filechooser");
const dropzone = page.locator("[class*='border-dashed']").first();
await dropzone.click();
const fileChooser = await fileChooserPromise;
await fileChooser.setFiles([FIXTURE_JPG, FIXTURE_PNG]);
await page.waitForTimeout(1000);
// Set resize width
await page.locator("input[placeholder='Auto']").first().fill("50");
// Process batch
await page.getByRole("button", { name: /resize.*2 files/i }).click();
await waitForProcessing(page, 30_000);
// Wait for result
await expect(page.locator("section[aria-label='Image area'] img").first()).toBeVisible({
timeout: 15_000,
});
// First result should have a download link
await expect(
page
.getByRole("link", { name: /download/i })
.or(page.getByRole("button", { name: /download$/i }))
.first(),
).toBeVisible();
// Navigate to second result
await page.getByRole("button", { name: "Next image" }).click();
await page.waitForTimeout(500);
// Second result should also have a download link
await expect(
page
.getByRole("link", { name: /download/i })
.or(page.getByRole("button", { name: /download$/i }))
.first(),
).toBeVisible();
});
test("undo on one image does not affect others", async ({ loggedInPage: page }) => {
await page.goto("/resize");
// Upload 2 images
const fileChooserPromise = page.waitForEvent("filechooser");
const dropzone = page.locator("[class*='border-dashed']").first();
await dropzone.click();
const fileChooser = await fileChooserPromise;
await fileChooser.setFiles([FIXTURE_JPG, FIXTURE_PNG]);
await page.waitForTimeout(1000);
// Set resize width
await page.locator("input[placeholder='Auto']").first().fill("50");
// Process batch
await page.getByRole("button", { name: /resize.*2 files/i }).click();
await waitForProcessing(page, 30_000);
// Wait for result
await expect(page.locator("section[aria-label='Image area'] img").first()).toBeVisible({
timeout: 15_000,
});
// Both images should be in processed state - files count should still be 2
await expect(page.getByText("Files (2)")).toBeVisible();
// Click undo (resets all processed state for all entries in the store)
const undoBtn = page.getByRole("button", { name: /undo|reset/i });
if (await undoBtn.isVisible({ timeout: 2000 }).catch(() => false)) {
await undoBtn.click();
await page.waitForTimeout(500);
// Files should still be loaded (not cleared)
await expect(page.getByText("Files (2)")).toBeVisible();
}
});
});
// ---------------------------------------------------------------------------
// Mixed formats
// ---------------------------------------------------------------------------
test.describe("Mixed formats", () => {
test("upload JPEG + PNG + WebP and all are accepted and shown", async ({
loggedInPage: page,
}) => {
await page.goto("/compress");
const fileChooserPromise = page.waitForEvent("filechooser");
const dropzone = page.locator("[class*='border-dashed']").first();
await dropzone.click();
const fileChooser = await fileChooserPromise;
await fileChooser.setFiles([FIXTURE_JPG, FIXTURE_PNG, FIXTURE_WEBP]);
await page.waitForTimeout(1000);
// All 3 files should be registered
await expect(page.getByText("Files (3)")).toBeVisible();
// Counter badge should show "1 / 3"
await expect(page.getByText("1 / 3")).toBeVisible();
// All 3 thumbnails should be visible in the thumbnail strip
await expect(page.locator("button[title='test-100x100.jpg']")).toBeVisible();
await expect(page.locator("button[title='test-200x150.png']")).toBeVisible();
await expect(page.locator("button[title='test-50x50.webp']")).toBeVisible();
});
});