Files
SnapOtter/tests/unit/web/tool-registry.test.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

437 lines
14 KiB
TypeScript

// @vitest-environment jsdom
import { describe, expect, it, vi } from "vitest";
// ---------------------------------------------------------------------------
// Mock all lazy-loaded tool settings components so we don't pull in the
// entire React component tree. Each dynamic import returns a minimal stub.
// ---------------------------------------------------------------------------
vi.mock("@/components/tools/resize-settings", () => ({
ResizeSettings: () => null,
}));
vi.mock("@/components/tools/crop-settings", () => ({
CropSettings: () => null,
}));
vi.mock("@/components/tools/rotate-settings", () => ({
RotateSettings: () => null,
}));
vi.mock("@/components/tools/convert-settings", () => ({
ConvertSettings: () => null,
}));
vi.mock("@/components/tools/compress-settings", () => ({
CompressSettings: () => null,
}));
vi.mock("@/components/tools/optimize-for-web-settings", () => ({
OptimizeForWebSettings: () => null,
}));
vi.mock("@/components/tools/strip-metadata-settings", () => ({
StripMetadataSettings: () => null,
}));
vi.mock("@/components/tools/edit-metadata-settings", () => ({
EditMetadataSettings: () => null,
}));
vi.mock("@/components/tools/color-settings", () => ({
ColorSettings: () => null,
}));
vi.mock("@/components/tools/sharpening-settings", () => ({
SharpeningSettings: () => null,
}));
vi.mock("@/components/tools/watermark-text-settings", () => ({
WatermarkTextSettings: () => null,
}));
vi.mock("@/components/tools/watermark-image-settings", () => ({
WatermarkImageSettings: () => null,
}));
vi.mock("@/components/tools/text-overlay-settings", () => ({
TextOverlaySettings: () => null,
}));
vi.mock("@/components/tools/compose-settings", () => ({
ComposeSettings: () => null,
}));
vi.mock("@/components/tools/info-settings", () => ({
InfoSettings: () => null,
}));
vi.mock("@/components/tools/compare-settings", () => ({
CompareSettings: () => null,
}));
vi.mock("@/components/tools/find-duplicates-settings", () => ({
FindDuplicatesSettings: () => null,
}));
vi.mock("@/components/tools/find-duplicates-results", () => ({
FindDuplicatesResults: () => null,
}));
vi.mock("@/components/tools/color-palette-settings", () => ({
ColorPaletteSettings: () => null,
}));
vi.mock("@/components/tools/qr-generate-settings", () => ({
QrGenerateSettings: () => null,
}));
vi.mock("@/components/tools/qr-generate-preview", () => ({
QrGeneratePreview: () => null,
}));
vi.mock("@/components/tools/barcode-read-settings", () => ({
BarcodeReadSettings: () => null,
}));
vi.mock("@/components/tools/image-to-base64-settings", () => ({
ImageToBase64Settings: () => null,
}));
vi.mock("@/components/tools/image-to-base64-results", () => ({
ImageToBase64Results: () => null,
}));
vi.mock("@/components/tools/collage-settings", () => ({
CollageSettings: () => null,
}));
vi.mock("@/components/tools/collage-preview", () => ({
CollagePreview: () => null,
}));
vi.mock("@/components/tools/stitch-settings", () => ({
StitchSettings: () => null,
}));
vi.mock("@/components/tools/split-settings", () => ({
SplitSettings: () => null,
}));
vi.mock("@/components/tools/split-canvas", () => ({
SplitCanvas: () => null,
}));
vi.mock("@/components/tools/border-settings", () => ({
BorderSettings: () => null,
}));
vi.mock("@/components/tools/svg-to-raster-settings", () => ({
SvgToRasterSettings: () => null,
}));
vi.mock("@/components/tools/vectorize-settings", () => ({
VectorizeSettings: () => null,
}));
vi.mock("@/components/tools/gif-tools-settings", () => ({
GifToolsSettings: () => null,
}));
vi.mock("@/components/tools/bulk-rename-settings", () => ({
BulkRenameSettings: () => null,
}));
vi.mock("@/components/tools/favicon-settings", () => ({
FaviconSettings: () => null,
}));
vi.mock("@/components/tools/image-to-pdf-settings", () => ({
ImageToPdfSettings: () => null,
}));
vi.mock("@/components/tools/pdf-to-image-settings", () => ({
PdfToImageSettings: () => null,
}));
vi.mock("@/components/tools/pdf-to-image-preview", () => ({
PdfToImagePreview: () => null,
}));
vi.mock("@/components/tools/replace-color-settings", () => ({
ReplaceColorSettings: () => null,
}));
vi.mock("@/components/tools/remove-bg-settings", () => ({
RemoveBgSettings: () => null,
}));
vi.mock("@/components/tools/upscale-settings", () => ({
UpscaleSettings: () => null,
}));
vi.mock("@/components/tools/ocr-settings", () => ({
OcrSettings: () => null,
}));
vi.mock("@/components/tools/blur-faces-settings", () => ({
BlurFacesSettings: () => null,
}));
vi.mock("@/components/tools/enhance-faces-settings", () => ({
EnhanceFacesSettings: () => null,
}));
vi.mock("@/components/tools/erase-object-settings", () => ({
EraseObjectSettings: () => null,
}));
vi.mock("@/components/tools/smart-crop-settings", () => ({
SmartCropSettings: () => null,
}));
vi.mock("@/components/tools/image-enhancement-settings", () => ({
ImageEnhancementSettings: () => null,
}));
vi.mock("@/components/tools/colorize-settings", () => ({
ColorizeSettings: () => null,
}));
vi.mock("@/components/tools/noise-removal-settings", () => ({
NoiseRemovalSettings: () => null,
}));
vi.mock("@/components/tools/passport-photo-settings", () => ({
PassportPhotoSettings: () => null,
PassportPhotoPreview: () => null,
}));
vi.mock("@/components/tools/red-eye-removal-settings", () => ({
RedEyeRemovalSettings: () => null,
}));
vi.mock("@/components/tools/restore-photo-settings", () => ({
RestorePhotoSettings: () => null,
}));
// ---------------------------------------------------------------------------
// Import after mocks
// ---------------------------------------------------------------------------
import type { DisplayMode } from "@/lib/tool-registry";
import { getToolRegistryEntry, toolRegistry } from "@/lib/tool-registry";
// ==========================================================================
// toolRegistry (Map)
// ==========================================================================
describe("toolRegistry", () => {
it("is a Map with entries", () => {
expect(toolRegistry).toBeInstanceOf(Map);
expect(toolRegistry.size).toBeGreaterThan(0);
});
it("contains all expected essential tool IDs", () => {
const essentials = [
"resize",
"crop",
"rotate",
"convert",
"compress",
"strip-metadata",
"edit-metadata",
];
for (const id of essentials) {
expect(toolRegistry.has(id), `missing tool: ${id}`).toBe(true);
}
});
it("contains AI tool IDs", () => {
const aiTools = [
"remove-background",
"upscale",
"ocr",
"blur-faces",
"enhance-faces",
"erase-object",
"smart-crop",
"image-enhancement",
"colorize",
"noise-removal",
"passport-photo",
"red-eye-removal",
"restore-photo",
];
for (const id of aiTools) {
expect(toolRegistry.has(id), `missing AI tool: ${id}`).toBe(true);
}
});
it("contains layout and composition tools", () => {
const layoutTools = ["collage", "stitch", "split", "border"];
for (const id of layoutTools) {
expect(toolRegistry.has(id), `missing layout tool: ${id}`).toBe(true);
}
});
it("contains utility tools", () => {
const utilityTools = [
"info",
"compare",
"find-duplicates",
"color-palette",
"qr-generate",
"barcode-read",
"image-to-base64",
];
for (const id of utilityTools) {
expect(toolRegistry.has(id), `missing utility tool: ${id}`).toBe(true);
}
});
it("contains format and conversion tools", () => {
const formatTools = [
"svg-to-raster",
"vectorize",
"gif-tools",
"bulk-rename",
"favicon",
"image-to-pdf",
"pdf-to-image",
"optimize-for-web",
];
for (const id of formatTools) {
expect(toolRegistry.has(id), `missing format tool: ${id}`).toBe(true);
}
});
it("every entry has a valid displayMode", () => {
const validModes: DisplayMode[] = [
"side-by-side",
"before-after",
"live-preview",
"no-comparison",
"interactive-crop",
"interactive-eraser",
"interactive-split",
"no-dropzone",
"custom-results",
];
for (const [toolId, entry] of toolRegistry) {
expect(validModes, `invalid displayMode for ${toolId}`).toContain(entry.displayMode);
}
});
it("every entry has a Settings component", () => {
for (const [toolId, entry] of toolRegistry) {
expect(entry.Settings, `missing Settings for ${toolId}`).toBeDefined();
expect(["function", "object"]).toContain(typeof entry.Settings);
}
});
it("tools with custom-results display mode have a ResultsPanel", () => {
for (const [toolId, entry] of toolRegistry) {
if (entry.displayMode === "custom-results") {
expect(entry.ResultsPanel, `missing ResultsPanel for ${toolId}`).toBeDefined();
}
}
});
it("tools with no-dropzone display mode have a ResultsPanel", () => {
for (const [toolId, entry] of toolRegistry) {
if (entry.displayMode === "no-dropzone") {
expect(entry.ResultsPanel, `missing ResultsPanel for ${toolId}`).toBeDefined();
}
}
});
it("rotate has livePreview enabled", () => {
const rotate = toolRegistry.get("rotate");
expect(rotate?.livePreview).toBe(true);
});
it("adjust-colors has livePreview enabled", () => {
const adjustColors = toolRegistry.get("adjust-colors");
expect(adjustColors?.livePreview).toBe(true);
});
it("border has livePreview enabled", () => {
const border = toolRegistry.get("border");
expect(border?.livePreview).toBe(true);
});
it("crop uses interactive-crop display mode", () => {
const crop = toolRegistry.get("crop");
expect(crop?.displayMode).toBe("interactive-crop");
});
it("erase-object uses interactive-eraser display mode", () => {
const eraseObject = toolRegistry.get("erase-object");
expect(eraseObject?.displayMode).toBe("interactive-eraser");
});
it("split uses interactive-split display mode", () => {
const split = toolRegistry.get("split");
expect(split?.displayMode).toBe("interactive-split");
});
});
// ==========================================================================
// getToolRegistryEntry
// ==========================================================================
describe("getToolRegistryEntry", () => {
it("returns the entry for a known tool", () => {
const entry = getToolRegistryEntry("resize");
expect(entry).toBeDefined();
expect(entry?.displayMode).toBe("side-by-side");
});
it("returns undefined for an unknown tool", () => {
expect(getToolRegistryEntry("nonexistent-tool")).toBeUndefined();
});
it("returns the correct entry for compress", () => {
const entry = getToolRegistryEntry("compress");
expect(entry).toBeDefined();
expect(entry?.displayMode).toBe("before-after");
});
it("returns entry with ResultsPanel for find-duplicates", () => {
const entry = getToolRegistryEntry("find-duplicates");
expect(entry).toBeDefined();
expect(entry?.displayMode).toBe("custom-results");
expect(entry?.ResultsPanel).toBeDefined();
});
it("returns entry with ResultsPanel for qr-generate", () => {
const entry = getToolRegistryEntry("qr-generate");
expect(entry).toBeDefined();
expect(entry?.displayMode).toBe("no-dropzone");
expect(entry?.ResultsPanel).toBeDefined();
});
});
// ==========================================================================
// Wrapper components (lines 305-324)
// ==========================================================================
import { render } from "@testing-library/react";
import React from "react";
function renderSettings(Settings: React.ComponentType<Record<string, unknown>>, props = {}) {
return render(
React.createElement(React.Suspense, { fallback: null }, React.createElement(Settings, props)),
);
}
describe("CropSettingsWrapper", () => {
it("renders null when cropProps is undefined", () => {
const entry = getToolRegistryEntry("crop");
expect(entry).toBeDefined();
const { container } = renderSettings(entry!.Settings as never);
expect(container.innerHTML).toBe("");
});
it("renders CropSettings when cropProps is provided", () => {
const entry = getToolRegistryEntry("crop");
expect(entry).toBeDefined();
const cropProps = {
cropState: {
crop: { x: 0, y: 0, width: 50, height: 50, unit: "%" },
aspect: undefined,
showGrid: true,
imgDimensions: { width: 200, height: 150 },
},
onCropChange: vi.fn(),
onAspectChange: vi.fn(),
onGridToggle: vi.fn(),
};
const { container } = renderSettings(entry!.Settings as never, { cropProps });
expect(container).toBeDefined();
});
});
describe("EraseObjectSettingsWrapper", () => {
it("renders null when eraserProps is undefined", () => {
const entry = getToolRegistryEntry("erase-object");
expect(entry).toBeDefined();
const { container } = renderSettings(entry!.Settings as never);
expect(container.innerHTML).toBe("");
});
it("renders EraseObjectSettings when eraserProps is provided", () => {
const entry = getToolRegistryEntry("erase-object");
expect(entry).toBeDefined();
const eraserProps = {
eraserRef: React.createRef(),
hasStrokes: false,
brushSize: 20,
onBrushSizeChange: vi.fn(),
};
const { container } = renderSettings(entry!.Settings as never, { eraserProps });
expect(container).toBeDefined();
});
});
describe("makeColorSettingsComponent", () => {
it("adjust-colors Settings renders without throwing", () => {
const entry = getToolRegistryEntry("adjust-colors");
expect(entry).toBeDefined();
const { container } = renderSettings(entry!.Settings as never, {
onPreviewFilter: vi.fn(),
});
expect(container).toBeDefined();
});
});