Files
SnapOtter/tests/unit/web/tool-registry.test.ts
T
SnapOtter f387e98fff feat: tool-first workflow polish -- fix tests, lint cleanup
- Remove obsolete fullscreen redirect test (route deleted in Task 3)
- Update dropzone test strings to match new i18n values
- Fix categories count in i18n locale test (12 -> 24)
- Add chart-maker to no-dropzone exempt list in tool-registry test
- Auto-fix import ordering (Biome) in popular tools route and landing config
2026-06-14 19:44:58 +08:00

600 lines
19 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,
}));
vi.mock("@/components/tools/transparency-fixer-settings", () => ({
TransparencyFixerSettings: () => null,
}));
vi.mock("@/components/tools/content-aware-resize-settings", () => ({
ContentAwareResizeSettings: () => null,
}));
vi.mock("@/components/tools/beautify-settings", () => ({
BeautifySettings: () => null,
}));
vi.mock("@/components/tools/meme-generator-settings", () => ({
MemeGeneratorSettings: () => null,
}));
vi.mock("@/components/tools/meme-generator-preview", () => ({
MemeGeneratorPreview: () => null,
}));
vi.mock("@/components/tools/color-blindness-settings", () => ({
ColorBlindnessSettings: () => null,
}));
vi.mock("@/components/tools/ai-canvas-expand-settings", () => ({
AiCanvasExpandSettings: () => null,
}));
vi.mock("@/components/tools/merge-pdf-settings", () => ({
MergePdfSettings: () => null,
}));
vi.mock("@/components/tools/split-pdf-settings", () => ({
SplitPdfSettings: () => null,
}));
vi.mock("@/components/tools/compress-pdf-settings", () => ({
CompressPdfSettings: () => null,
}));
vi.mock("@/components/tools/rotate-pdf-settings", () => ({
RotatePdfSettings: () => null,
}));
vi.mock("@/components/tools/word-to-pdf-settings", () => ({
WordToPdfSettings: () => null,
}));
vi.mock("@/components/tools/csv-excel-settings", () => ({
CsvExcelSettings: () => null,
}));
vi.mock("@/components/tools/csv-json-settings", () => ({
CsvJsonSettings: () => null,
}));
vi.mock("@/components/tools/json-xml-settings", () => ({
JsonXmlSettings: () => null,
}));
vi.mock("@/components/tools/split-csv-settings", () => ({
SplitCsvSettings: () => null,
}));
// Wave 5a: Image gap-fill tools
vi.mock("@/components/tools/barcode-generate-settings", () => ({
BarcodeGenerateSettings: () => null,
}));
vi.mock("@/components/tools/chart-maker-settings", () => ({
ChartMakerSettings: () => null,
}));
vi.mock("@/components/tools/circle-crop-settings", () => ({
CircleCropSettings: () => null,
}));
vi.mock("@/components/tools/duotone-settings", () => ({
DuotoneSettings: () => null,
}));
vi.mock("@/components/tools/gif-webp-settings", () => ({
GifWebpSettings: () => null,
}));
vi.mock("@/components/tools/histogram-settings", () => ({
HistogramSettings: () => null,
}));
vi.mock("@/components/tools/image-pad-settings", () => ({
ImagePadSettings: () => null,
}));
vi.mock("@/components/tools/lqip-placeholder-settings", () => ({
LqipPlaceholderSettings: () => null,
}));
vi.mock("@/components/tools/pixelate-settings", () => ({
PixelateSettings: () => null,
}));
vi.mock("@/components/tools/sprite-sheet-settings", () => ({
SpriteSheetSettings: () => null,
}));
vi.mock("@/components/tools/vignette-settings", () => ({
VignetteSettings: () => null,
}));
// ---------------------------------------------------------------------------
// Import after mocks
// ---------------------------------------------------------------------------
import { TOOLS } from "@snapotter/shared";
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",
"content-aware-resize",
];
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",
"media-player",
"document",
];
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", () => {
const selfContainedTools = new Set(["meme-generator", "barcode-generate", "chart-maker"]);
for (const [toolId, entry] of toolRegistry) {
if (entry.displayMode === "no-dropzone" && !selfContainedTools.has(toolId)) {
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("beautify has livePreview enabled", () => {
const beautify = toolRegistry.get("beautify");
expect(beautify).toBeDefined();
expect(beautify?.livePreview).toBe(true);
expect(beautify?.displayMode).toBe("live-preview");
});
it("meme-generator has no-dropzone display mode with ResultsPanel", () => {
const meme = toolRegistry.get("meme-generator");
expect(meme).toBeDefined();
expect(meme?.displayMode).toBe("no-dropzone");
expect(meme?.ResultsPanel).toBeDefined();
expect(meme?.Settings).toBeDefined();
});
it("color-blindness uses before-after display mode", () => {
const cb = toolRegistry.get("color-blindness");
expect(cb).toBeDefined();
expect(cb?.displayMode).toBe("before-after");
expect(cb?.Settings).toBeDefined();
});
it("transparency-fixer uses before-after display mode", () => {
const tf = toolRegistry.get("transparency-fixer");
expect(tf).toBeDefined();
expect(tf?.displayMode).toBe("before-after");
expect(tf?.Settings).toBeDefined();
});
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");
});
it("every tool in shared TOOLS[] has a matching registry entry", () => {
const missing: string[] = [];
for (const tool of TOOLS) {
if (!toolRegistry.has(tool.id)) {
missing.push(tool.id);
}
}
expect(
missing,
`Tools defined in TOOLS[] but missing from toolRegistry: ${missing.join(", ")}`,
).toEqual([]);
});
});
// ==========================================================================
// 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();
});
it("returns entry for content-aware-resize with side-by-side display (regression #131)", () => {
const entry = getToolRegistryEntry("content-aware-resize");
expect(entry).toBeDefined();
expect(entry?.displayMode).toBe("side-by-side");
expect(entry?.Settings).toBeDefined();
});
it("returns entry for beautify with live-preview display", () => {
const entry = getToolRegistryEntry("beautify");
expect(entry).toBeDefined();
expect(entry?.displayMode).toBe("live-preview");
expect(entry?.livePreview).toBe(true);
});
it("returns entry for meme-generator with ResultsPanel", () => {
const entry = getToolRegistryEntry("meme-generator");
expect(entry).toBeDefined();
expect(entry?.displayMode).toBe("no-dropzone");
expect(entry?.ResultsPanel).toBeDefined();
});
it("returns entry for color-blindness", () => {
const entry = getToolRegistryEntry("color-blindness");
expect(entry).toBeDefined();
expect(entry?.displayMode).toBe("before-after");
});
it("no duplicate tool IDs in registry", () => {
const ids = [...toolRegistry.keys()];
const unique = new Set(ids);
expect(ids.length).toBe(unique.size);
});
});
// ==========================================================================
// 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();
});
});