mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
test: testing overhaul -- CI e2e gates, parallel suites, generated matrices, mutation testing (#215)
Closes the "e2e never runs in CI" hole. Adds per-PR e2e smoke gate, nightly full-suite workflows, parallel vitest forks (per-fork DBs), Playwright parallel/serial/visual projects against production builds, metadata-generated test suites (drift guards, hostile inputs, format matrix, pairwise settings, property-based fuzz), Stryker mutation testing, Schemathesis API fuzz, coverage ratchet, and fixes for three session-poisoning bugs that caused 200+ serial-bucket failures. Bug fix included: favicon/split/bulk-rename could hang clients forever when ZIP streaming failed after reply.hijack().
This commit is contained in:
@@ -0,0 +1,91 @@
|
||||
/**
|
||||
* Pure display-mode map: toolId -> DisplayMode.
|
||||
*
|
||||
* This module intentionally has no React imports so that Playwright specs and
|
||||
* Node-side test generators can import it directly. It is the single source of
|
||||
* truth for display modes; tool-registry.tsx merges it into registry entries.
|
||||
*/
|
||||
|
||||
export type DisplayMode =
|
||||
| "side-by-side"
|
||||
| "before-after"
|
||||
| "live-preview"
|
||||
| "no-comparison"
|
||||
| "interactive-crop"
|
||||
| "interactive-eraser"
|
||||
| "interactive-split"
|
||||
| "no-dropzone"
|
||||
| "custom-results";
|
||||
|
||||
export const TOOL_DISPLAY_MODES: Record<string, DisplayMode> = {
|
||||
// Essentials
|
||||
resize: "side-by-side",
|
||||
crop: "interactive-crop",
|
||||
rotate: "side-by-side",
|
||||
convert: "no-comparison",
|
||||
compress: "before-after",
|
||||
"strip-metadata": "no-comparison",
|
||||
"edit-metadata": "no-comparison",
|
||||
|
||||
// Color adjustments
|
||||
"adjust-colors": "live-preview",
|
||||
sharpening: "before-after",
|
||||
|
||||
// Watermark & overlay
|
||||
"watermark-text": "before-after",
|
||||
"watermark-image": "before-after",
|
||||
"text-overlay": "before-after",
|
||||
compose: "before-after",
|
||||
"meme-generator": "no-dropzone",
|
||||
|
||||
// Utilities
|
||||
info: "no-comparison",
|
||||
compare: "before-after",
|
||||
"find-duplicates": "custom-results",
|
||||
"color-palette": "no-comparison",
|
||||
"qr-generate": "no-dropzone",
|
||||
"html-to-image": "no-dropzone",
|
||||
"barcode-read": "before-after",
|
||||
"image-to-base64": "custom-results",
|
||||
|
||||
// Layout & composition
|
||||
collage: "no-dropzone",
|
||||
stitch: "no-comparison",
|
||||
split: "interactive-split",
|
||||
border: "live-preview",
|
||||
beautify: "live-preview",
|
||||
|
||||
// Format & conversion
|
||||
"svg-to-raster": "before-after",
|
||||
vectorize: "before-after",
|
||||
"gif-tools": "before-after",
|
||||
|
||||
// Optimization extras
|
||||
"bulk-rename": "before-after",
|
||||
favicon: "before-after",
|
||||
"image-to-pdf": "before-after",
|
||||
"optimize-for-web": "before-after",
|
||||
"pdf-to-image": "no-dropzone",
|
||||
|
||||
// Adjustments extra
|
||||
"replace-color": "before-after",
|
||||
"color-blindness": "before-after",
|
||||
|
||||
// AI tools
|
||||
"remove-background": "before-after",
|
||||
upscale: "before-after",
|
||||
ocr: "before-after",
|
||||
"blur-faces": "before-after",
|
||||
"enhance-faces": "before-after",
|
||||
"erase-object": "interactive-eraser",
|
||||
"smart-crop": "before-after",
|
||||
"image-enhancement": "before-after",
|
||||
colorize: "before-after",
|
||||
"noise-removal": "before-after",
|
||||
"passport-photo": "custom-results",
|
||||
"red-eye-removal": "before-after",
|
||||
"restore-photo": "before-after",
|
||||
"transparency-fixer": "before-after",
|
||||
"content-aware-resize": "side-by-side",
|
||||
"ai-canvas-expand": "before-after",
|
||||
};
|
||||
@@ -10,19 +10,16 @@ import type { Crop } from "react-image-crop";
|
||||
import type { BgPreviewState } from "@/components/common/image-viewer";
|
||||
import type { EraserCanvasRef } from "@/components/tools/eraser-canvas";
|
||||
import type { PreviewTransform } from "@/components/tools/rotate-settings";
|
||||
import { TOOL_DISPLAY_MODES } from "./tool-display-modes";
|
||||
|
||||
// ── Display modes ──────────────────────────────────────────────────
|
||||
// The DisplayMode type and per-tool map live in tool-display-modes.ts (a pure
|
||||
// module with no React imports) so tests can introspect them. Re-exported here
|
||||
// for existing importers.
|
||||
|
||||
export type DisplayMode =
|
||||
| "side-by-side"
|
||||
| "before-after"
|
||||
| "live-preview"
|
||||
| "no-comparison"
|
||||
| "interactive-crop"
|
||||
| "interactive-eraser"
|
||||
| "interactive-split"
|
||||
| "no-dropzone"
|
||||
| "custom-results";
|
||||
export type { DisplayMode } from "./tool-display-modes";
|
||||
|
||||
import type { DisplayMode } from "./tool-display-modes";
|
||||
|
||||
// ── Crop and eraser prop types ─────────────────────────────────────
|
||||
|
||||
@@ -371,161 +368,100 @@ function EraseObjectSettingsWrapper(props: { eraserProps?: EraserProps }) {
|
||||
}
|
||||
|
||||
// ── The registry ───────────────────────────────────────────────────
|
||||
// Display modes come from TOOL_DISPLAY_MODES (tool-display-modes.ts); entries
|
||||
// here hold only the React-side pieces (Settings, ResultsPanel, livePreview,
|
||||
// accept). The merge below throws at module load if either side is missing a
|
||||
// tool, so drift between the two files cannot ship.
|
||||
|
||||
export const toolRegistry = new Map<string, ToolRegistryEntry>([
|
||||
type RegistryEntryConfig = Omit<ToolRegistryEntry, "displayMode">;
|
||||
|
||||
const ENTRY_CONFIG: ReadonlyArray<[string, RegistryEntryConfig]> = [
|
||||
// Essentials
|
||||
["resize", { displayMode: "side-by-side", Settings: ResizeSettings }],
|
||||
["crop", { displayMode: "interactive-crop", Settings: CropSettingsWrapper as never }],
|
||||
[
|
||||
"rotate",
|
||||
{
|
||||
displayMode: "side-by-side",
|
||||
livePreview: true,
|
||||
Settings: RotateSettings as never,
|
||||
},
|
||||
],
|
||||
["convert", { displayMode: "no-comparison", Settings: ConvertSettings }],
|
||||
["compress", { displayMode: "before-after", Settings: CompressSettings }],
|
||||
["strip-metadata", { displayMode: "no-comparison", Settings: StripMetadataSettings }],
|
||||
["edit-metadata", { displayMode: "no-comparison", Settings: EditMetadataSettings }],
|
||||
["resize", { Settings: ResizeSettings }],
|
||||
["crop", { Settings: CropSettingsWrapper as never }],
|
||||
["rotate", { livePreview: true, Settings: RotateSettings as never }],
|
||||
["convert", { Settings: ConvertSettings }],
|
||||
["compress", { Settings: CompressSettings }],
|
||||
["strip-metadata", { Settings: StripMetadataSettings }],
|
||||
["edit-metadata", { Settings: EditMetadataSettings }],
|
||||
|
||||
// Color adjustments (consolidated)
|
||||
[
|
||||
"adjust-colors",
|
||||
{
|
||||
displayMode: "live-preview" as DisplayMode,
|
||||
livePreview: true,
|
||||
Settings: makeColorSettingsComponent("adjust-colors") as never,
|
||||
},
|
||||
{ livePreview: true, Settings: makeColorSettingsComponent("adjust-colors") as never },
|
||||
],
|
||||
|
||||
// Sharpening
|
||||
["sharpening", { displayMode: "before-after", Settings: SharpeningSettings }],
|
||||
["sharpening", { Settings: SharpeningSettings }],
|
||||
|
||||
// Watermark & Overlay
|
||||
["watermark-text", { displayMode: "before-after", Settings: WatermarkTextSettings }],
|
||||
["watermark-image", { displayMode: "before-after", Settings: WatermarkImageSettings }],
|
||||
["text-overlay", { displayMode: "before-after", Settings: TextOverlaySettings }],
|
||||
["compose", { displayMode: "before-after", Settings: ComposeSettings }],
|
||||
[
|
||||
"meme-generator",
|
||||
{
|
||||
displayMode: "no-dropzone",
|
||||
Settings: MemeGeneratorSettings,
|
||||
ResultsPanel: MemeGeneratorPreview,
|
||||
},
|
||||
],
|
||||
["watermark-text", { Settings: WatermarkTextSettings }],
|
||||
["watermark-image", { Settings: WatermarkImageSettings }],
|
||||
["text-overlay", { Settings: TextOverlaySettings }],
|
||||
["compose", { Settings: ComposeSettings }],
|
||||
["meme-generator", { Settings: MemeGeneratorSettings, ResultsPanel: MemeGeneratorPreview }],
|
||||
|
||||
// Utilities
|
||||
["info", { displayMode: "no-comparison", Settings: InfoSettings }],
|
||||
["compare", { displayMode: "before-after", Settings: CompareSettings }],
|
||||
[
|
||||
"find-duplicates",
|
||||
{
|
||||
displayMode: "custom-results",
|
||||
Settings: FindDuplicatesSettings,
|
||||
ResultsPanel: FindDuplicatesResults,
|
||||
},
|
||||
],
|
||||
["color-palette", { displayMode: "no-comparison", Settings: ColorPaletteSettings }],
|
||||
[
|
||||
"qr-generate",
|
||||
{ displayMode: "no-dropzone", Settings: QrGenerateSettings, ResultsPanel: QrGeneratePreview },
|
||||
],
|
||||
[
|
||||
"html-to-image",
|
||||
{
|
||||
displayMode: "no-dropzone",
|
||||
Settings: HtmlToImageSettings,
|
||||
ResultsPanel: HtmlToImageResults,
|
||||
},
|
||||
],
|
||||
["barcode-read", { displayMode: "before-after", Settings: BarcodeReadSettings }],
|
||||
[
|
||||
"image-to-base64",
|
||||
{
|
||||
displayMode: "custom-results",
|
||||
Settings: ImageToBase64Settings,
|
||||
ResultsPanel: ImageToBase64Results,
|
||||
},
|
||||
],
|
||||
["info", { Settings: InfoSettings }],
|
||||
["compare", { Settings: CompareSettings }],
|
||||
["find-duplicates", { Settings: FindDuplicatesSettings, ResultsPanel: FindDuplicatesResults }],
|
||||
["color-palette", { Settings: ColorPaletteSettings }],
|
||||
["qr-generate", { Settings: QrGenerateSettings, ResultsPanel: QrGeneratePreview }],
|
||||
["html-to-image", { Settings: HtmlToImageSettings, ResultsPanel: HtmlToImageResults }],
|
||||
["barcode-read", { Settings: BarcodeReadSettings }],
|
||||
["image-to-base64", { Settings: ImageToBase64Settings, ResultsPanel: ImageToBase64Results }],
|
||||
|
||||
// Layout & Composition
|
||||
[
|
||||
"collage",
|
||||
{ displayMode: "no-dropzone", Settings: CollageSettings, ResultsPanel: CollagePreview },
|
||||
],
|
||||
["stitch", { displayMode: "no-comparison", Settings: StitchSettings }],
|
||||
[
|
||||
"split",
|
||||
{ displayMode: "interactive-split", Settings: SplitSettings, ResultsPanel: SplitCanvas },
|
||||
],
|
||||
["border", { displayMode: "live-preview", livePreview: true, Settings: BorderSettings as never }],
|
||||
[
|
||||
"beautify",
|
||||
{ displayMode: "live-preview", livePreview: true, Settings: BeautifySettings as never },
|
||||
],
|
||||
["collage", { Settings: CollageSettings, ResultsPanel: CollagePreview }],
|
||||
["stitch", { Settings: StitchSettings }],
|
||||
["split", { Settings: SplitSettings, ResultsPanel: SplitCanvas }],
|
||||
["border", { livePreview: true, Settings: BorderSettings as never }],
|
||||
["beautify", { livePreview: true, Settings: BeautifySettings as never }],
|
||||
|
||||
// Format & Conversion
|
||||
[
|
||||
"svg-to-raster",
|
||||
{ displayMode: "before-after", accept: ".svg,.svgz", Settings: SvgToRasterSettings },
|
||||
],
|
||||
["vectorize", { displayMode: "before-after", Settings: VectorizeSettings }],
|
||||
["gif-tools", { displayMode: "before-after", Settings: GifToolsSettings }],
|
||||
["svg-to-raster", { accept: ".svg,.svgz", Settings: SvgToRasterSettings }],
|
||||
["vectorize", { Settings: VectorizeSettings }],
|
||||
["gif-tools", { Settings: GifToolsSettings }],
|
||||
|
||||
// Optimization extras
|
||||
["bulk-rename", { displayMode: "before-after", Settings: BulkRenameSettings }],
|
||||
["favicon", { displayMode: "before-after", Settings: FaviconSettings }],
|
||||
["image-to-pdf", { displayMode: "before-after", Settings: ImageToPdfSettings }],
|
||||
["optimize-for-web", { displayMode: "before-after", Settings: OptimizeForWebSettings }],
|
||||
[
|
||||
"pdf-to-image",
|
||||
{ displayMode: "no-dropzone", Settings: PdfToImageSettings, ResultsPanel: PdfToImagePreview },
|
||||
],
|
||||
["bulk-rename", { Settings: BulkRenameSettings }],
|
||||
["favicon", { Settings: FaviconSettings }],
|
||||
["image-to-pdf", { Settings: ImageToPdfSettings }],
|
||||
["optimize-for-web", { Settings: OptimizeForWebSettings }],
|
||||
["pdf-to-image", { Settings: PdfToImageSettings, ResultsPanel: PdfToImagePreview }],
|
||||
|
||||
// Adjustments extra
|
||||
["replace-color", { displayMode: "before-after", Settings: ReplaceColorSettings }],
|
||||
["color-blindness", { displayMode: "before-after", Settings: ColorBlindnessSettings }],
|
||||
["replace-color", { Settings: ReplaceColorSettings }],
|
||||
["color-blindness", { Settings: ColorBlindnessSettings }],
|
||||
|
||||
// AI Tools
|
||||
["remove-background", { displayMode: "before-after", Settings: RemoveBgSettings }],
|
||||
["upscale", { displayMode: "before-after", Settings: UpscaleSettings }],
|
||||
["ocr", { displayMode: "before-after", Settings: OcrSettings }],
|
||||
["blur-faces", { displayMode: "before-after", Settings: BlurFacesSettings }],
|
||||
["enhance-faces", { displayMode: "before-after", Settings: EnhanceFacesSettings }],
|
||||
[
|
||||
"erase-object",
|
||||
{
|
||||
displayMode: "interactive-eraser",
|
||||
Settings: EraseObjectSettingsWrapper as never,
|
||||
},
|
||||
],
|
||||
["smart-crop", { displayMode: "before-after", Settings: SmartCropSettings }],
|
||||
[
|
||||
"image-enhancement",
|
||||
{
|
||||
displayMode: "before-after" as DisplayMode,
|
||||
livePreview: true,
|
||||
Settings: ImageEnhancementSettings as never,
|
||||
},
|
||||
],
|
||||
["colorize", { displayMode: "before-after", Settings: ColorizeSettings }],
|
||||
["noise-removal", { displayMode: "before-after", Settings: NoiseRemovalSettings }],
|
||||
[
|
||||
"passport-photo",
|
||||
{
|
||||
displayMode: "custom-results",
|
||||
Settings: PassportPhotoSettings,
|
||||
ResultsPanel: PassportPhotoPreview,
|
||||
},
|
||||
],
|
||||
["red-eye-removal", { displayMode: "before-after", Settings: RedEyeRemovalSettings }],
|
||||
["restore-photo", { displayMode: "before-after", Settings: RestorePhotoSettings }],
|
||||
["transparency-fixer", { displayMode: "before-after", Settings: TransparencyFixerSettings }],
|
||||
["content-aware-resize", { displayMode: "side-by-side", Settings: ContentAwareResizeSettings }],
|
||||
["ai-canvas-expand", { displayMode: "before-after", Settings: AiCanvasExpandSettings }],
|
||||
]);
|
||||
["remove-background", { Settings: RemoveBgSettings }],
|
||||
["upscale", { Settings: UpscaleSettings }],
|
||||
["ocr", { Settings: OcrSettings }],
|
||||
["blur-faces", { Settings: BlurFacesSettings }],
|
||||
["enhance-faces", { Settings: EnhanceFacesSettings }],
|
||||
["erase-object", { Settings: EraseObjectSettingsWrapper as never }],
|
||||
["smart-crop", { Settings: SmartCropSettings }],
|
||||
["image-enhancement", { livePreview: true, Settings: ImageEnhancementSettings as never }],
|
||||
["colorize", { Settings: ColorizeSettings }],
|
||||
["noise-removal", { Settings: NoiseRemovalSettings }],
|
||||
["passport-photo", { Settings: PassportPhotoSettings, ResultsPanel: PassportPhotoPreview }],
|
||||
["red-eye-removal", { Settings: RedEyeRemovalSettings }],
|
||||
["restore-photo", { Settings: RestorePhotoSettings }],
|
||||
["transparency-fixer", { Settings: TransparencyFixerSettings }],
|
||||
["content-aware-resize", { Settings: ContentAwareResizeSettings }],
|
||||
["ai-canvas-expand", { Settings: AiCanvasExpandSettings }],
|
||||
];
|
||||
|
||||
export const toolRegistry = new Map<string, ToolRegistryEntry>(
|
||||
ENTRY_CONFIG.map(([toolId, entry]) => {
|
||||
const displayMode = TOOL_DISPLAY_MODES[toolId];
|
||||
if (!displayMode) {
|
||||
throw new Error(`Tool "${toolId}" has no display mode in tool-display-modes.ts`);
|
||||
}
|
||||
return [toolId, { ...entry, displayMode }];
|
||||
}),
|
||||
);
|
||||
|
||||
export function getToolRegistryEntry(toolId: string): ToolRegistryEntry | undefined {
|
||||
return toolRegistry.get(toolId);
|
||||
|
||||
@@ -22,6 +22,19 @@ export default defineConfig({
|
||||
},
|
||||
},
|
||||
},
|
||||
// vite preview serves the production build for e2e runs; it needs the same
|
||||
// /api proxy the dev server has (the app always calls relative /api).
|
||||
preview: {
|
||||
host: true,
|
||||
port: Number(process.env.PORT) || 1351,
|
||||
proxy: {
|
||||
"/api": {
|
||||
target: process.env.VITE_API_URL || "http://localhost:13490",
|
||||
timeout: 300_000,
|
||||
proxyTimeout: 300_000,
|
||||
},
|
||||
},
|
||||
},
|
||||
build: {
|
||||
rollupOptions: {},
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user