mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
Fix 2 failing unit tests (landing hero text mismatch) and broken coverage tooling (brace-expansion v5 override breaking minimatch). Add ~1,097 new test cases via 14-agent parallel expansion: - Unit: +290 tests (AI bridge, image-engine, stores, API helpers) - Integration: +504 tests (all tools, cross-format matrix, adversarial) - E2E: +363 tests (navigation, tool UI, batch/pipeline, settings, visual regression, accessibility, performance, cross-browser) Total: 4,223 unit + 6,057 integration + 1,563 E2E = 11,843 tests
523 lines
17 KiB
TypeScript
523 lines
17 KiB
TypeScript
// @vitest-environment jsdom
|
|
/**
|
|
* Expanded store tests covering uncovered branches:
|
|
* - QR store: gradient settings, corner colors, custom dot types
|
|
* - Split store: edge cases for effective grid, computed dimensions
|
|
* - Pipeline store: reorder edge cases
|
|
* - PDF store: compressPageRange edge cases
|
|
* - Duplicate store: view mode transitions
|
|
* - Base64 store: concurrent adds
|
|
*/
|
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
|
|
vi.mock("zustand/middleware", async (importOriginal) => {
|
|
const actual: Record<string, unknown> = await importOriginal();
|
|
return {
|
|
...actual,
|
|
persist: (config: unknown) => config,
|
|
};
|
|
});
|
|
|
|
const revokeObjectURL = vi.fn();
|
|
const createObjectURL = vi.fn((_obj: Blob | MediaSource) => "blob:fake-url");
|
|
|
|
vi.stubGlobal("URL", {
|
|
...globalThis.URL,
|
|
createObjectURL,
|
|
revokeObjectURL,
|
|
});
|
|
|
|
vi.stubGlobal("fetch", vi.fn());
|
|
|
|
vi.stubGlobal("localStorage", {
|
|
getItem: vi.fn(() => null),
|
|
setItem: vi.fn(),
|
|
removeItem: vi.fn(),
|
|
clear: vi.fn(),
|
|
get length() {
|
|
return 0;
|
|
},
|
|
key: vi.fn((_i: number) => null),
|
|
});
|
|
|
|
vi.stubGlobal(
|
|
"matchMedia",
|
|
vi.fn().mockReturnValue({
|
|
matches: false,
|
|
addEventListener: vi.fn(),
|
|
removeEventListener: vi.fn(),
|
|
}),
|
|
);
|
|
|
|
vi.mock("@/lib/api", () => ({
|
|
apiGet: vi.fn(),
|
|
apiPost: vi.fn(),
|
|
apiPut: vi.fn(),
|
|
apiDelete: vi.fn(),
|
|
formatHeaders: vi.fn(() => new Headers()),
|
|
}));
|
|
|
|
vi.mock("@/lib/collage-templates", () => ({
|
|
COLLAGE_TEMPLATES: [
|
|
{
|
|
id: "2-h-equal",
|
|
imageCount: 2,
|
|
cells: [
|
|
{ gridColumn: "1", gridRow: "1" },
|
|
{ gridColumn: "2", gridRow: "1" },
|
|
],
|
|
},
|
|
],
|
|
getDefaultTemplate: () => ({
|
|
id: "2-h-equal",
|
|
imageCount: 2,
|
|
cells: [
|
|
{ gridColumn: "1", gridRow: "1" },
|
|
{ gridColumn: "2", gridRow: "1" },
|
|
],
|
|
}),
|
|
}));
|
|
|
|
vi.mock("@/lib/image-preview", () => ({
|
|
needsServerPreview: vi.fn(() => false),
|
|
fetchDecodedPreview: vi.fn(() => Promise.resolve(null)),
|
|
revokePreviewUrl: vi.fn(),
|
|
}));
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// QR Store expanded
|
|
// ---------------------------------------------------------------------------
|
|
import { encodeQrData, useQrStore } from "@/stores/qr-store";
|
|
|
|
describe("useQrStore expanded", () => {
|
|
beforeEach(() => {
|
|
useQrStore.getState().reset();
|
|
});
|
|
|
|
it("setDotGradientEnabled toggles gradient", () => {
|
|
useQrStore.getState().setDotGradientEnabled(true);
|
|
expect(useQrStore.getState().dotGradientEnabled).toBe(true);
|
|
});
|
|
|
|
it("setDotGradientType changes gradient type", () => {
|
|
useQrStore.getState().setDotGradientType("radial");
|
|
expect(useQrStore.getState().dotGradientType).toBe("radial");
|
|
});
|
|
|
|
it("setDotGradientColor1 updates first gradient color", () => {
|
|
useQrStore.getState().setDotGradientColor1("#FF0000");
|
|
expect(useQrStore.getState().dotGradientColor1).toBe("#FF0000");
|
|
});
|
|
|
|
it("setDotGradientColor2 updates second gradient color", () => {
|
|
useQrStore.getState().setDotGradientColor2("#0000FF");
|
|
expect(useQrStore.getState().dotGradientColor2).toBe("#0000FF");
|
|
});
|
|
|
|
it("setDotGradientRotation updates rotation", () => {
|
|
useQrStore.getState().setDotGradientRotation(45);
|
|
expect(useQrStore.getState().dotGradientRotation).toBe(45);
|
|
});
|
|
|
|
it("setCornerSquareColor updates corner square color", () => {
|
|
useQrStore.getState().setCornerSquareColor("#00FF00");
|
|
expect(useQrStore.getState().cornerSquareColor).toBe("#00FF00");
|
|
});
|
|
|
|
it("setCornerDotColor updates corner dot color", () => {
|
|
useQrStore.getState().setCornerDotColor("#FFFF00");
|
|
expect(useQrStore.getState().cornerDotColor).toBe("#FFFF00");
|
|
});
|
|
|
|
it("setUseCustomCornerColors toggles custom corner colors", () => {
|
|
useQrStore.getState().setUseCustomCornerColors(true);
|
|
expect(useQrStore.getState().useCustomCornerColors).toBe(true);
|
|
});
|
|
|
|
it("setDotType changes dot type", () => {
|
|
useQrStore.getState().setDotType("dots");
|
|
expect(useQrStore.getState().dotType).toBe("dots");
|
|
});
|
|
|
|
it("setCornerSquareType changes corner square type", () => {
|
|
useQrStore.getState().setCornerSquareType("dot");
|
|
expect(useQrStore.getState().cornerSquareType).toBe("dot");
|
|
});
|
|
|
|
it("setCornerDotType changes corner dot type", () => {
|
|
useQrStore.getState().setCornerDotType("square");
|
|
expect(useQrStore.getState().cornerDotType).toBe("square");
|
|
});
|
|
|
|
it("setLogoSize updates logo size", () => {
|
|
useQrStore.getState().setLogoSize(0.3);
|
|
expect(useQrStore.getState().logoSize).toBe(0.3);
|
|
});
|
|
|
|
it("setLogoMargin updates logo margin", () => {
|
|
useQrStore.getState().setLogoMargin(10);
|
|
expect(useQrStore.getState().logoMargin).toBe(10);
|
|
});
|
|
|
|
it("setHideBackgroundDots toggles background dots visibility", () => {
|
|
useQrStore.getState().setHideBackgroundDots(false);
|
|
expect(useQrStore.getState().hideBackgroundDots).toBe(false);
|
|
});
|
|
|
|
it("setPhoneData updates phone data", () => {
|
|
useQrStore.getState().setPhoneData("+15551234567");
|
|
expect(useQrStore.getState().phoneData).toBe("+15551234567");
|
|
});
|
|
|
|
it("setBgColor updates background color", () => {
|
|
useQrStore.getState().setBgColor("#CCCCCC");
|
|
expect(useQrStore.getState().bgColor).toBe("#CCCCCC");
|
|
});
|
|
});
|
|
|
|
describe("encodeQrData expanded", () => {
|
|
it("encodes wifi with WEP encryption", () => {
|
|
const state = {
|
|
...useQrStore.getState(),
|
|
contentType: "wifi" as const,
|
|
wifiData: { ssid: "Net", password: "key", encryption: "WEP" as const, hidden: true },
|
|
};
|
|
const result = encodeQrData(state);
|
|
expect(result).toBe("WIFI:T:WEP;S:Net;P:key;H:true;;");
|
|
});
|
|
|
|
it("encodes wifi with no password (nopass)", () => {
|
|
const state = {
|
|
...useQrStore.getState(),
|
|
contentType: "wifi" as const,
|
|
wifiData: { ssid: "Open", password: "", encryption: "nopass" as const, hidden: false },
|
|
};
|
|
expect(encodeQrData(state)).toBe("WIFI:T:nopass;S:Open;P:;H:false;;");
|
|
});
|
|
|
|
it("encodes email with only subject (no body)", () => {
|
|
const state = {
|
|
...useQrStore.getState(),
|
|
contentType: "email" as const,
|
|
emailData: { to: "a@b.com", subject: "Test", body: "" },
|
|
};
|
|
expect(encodeQrData(state)).toBe("mailto:a@b.com?subject=Test");
|
|
});
|
|
|
|
it("encodes email with only body (no subject)", () => {
|
|
const state = {
|
|
...useQrStore.getState(),
|
|
contentType: "email" as const,
|
|
emailData: { to: "a@b.com", subject: "", body: "Hello" },
|
|
};
|
|
expect(encodeQrData(state)).toBe("mailto:a@b.com?body=Hello");
|
|
});
|
|
|
|
it("encodes vcard with only first name", () => {
|
|
const state = {
|
|
...useQrStore.getState(),
|
|
contentType: "vcard" as const,
|
|
vcardData: {
|
|
firstName: "Alice",
|
|
lastName: "",
|
|
phone: "",
|
|
email: "",
|
|
organization: "",
|
|
title: "",
|
|
url: "",
|
|
},
|
|
};
|
|
const result = encodeQrData(state);
|
|
expect(result).toContain("N:;Alice");
|
|
expect(result).toContain("FN:Alice");
|
|
expect(result).not.toContain("TEL:");
|
|
});
|
|
|
|
it("encodes sms with empty message", () => {
|
|
const state = {
|
|
...useQrStore.getState(),
|
|
contentType: "sms" as const,
|
|
smsData: { phone: "+1234", message: "" },
|
|
};
|
|
expect(encodeQrData(state)).toBe("smsto:+1234:");
|
|
});
|
|
|
|
it("encodes empty text data", () => {
|
|
const state = {
|
|
...useQrStore.getState(),
|
|
contentType: "text" as const,
|
|
textData: "",
|
|
};
|
|
expect(encodeQrData(state)).toBe("");
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Split Store expanded
|
|
// ---------------------------------------------------------------------------
|
|
import { useSplitStore } from "@/stores/split-store";
|
|
|
|
describe("useSplitStore expanded", () => {
|
|
beforeEach(() => {
|
|
revokeObjectURL.mockClear();
|
|
useSplitStore.setState({
|
|
mode: "grid",
|
|
columns: 3,
|
|
rows: 3,
|
|
tileWidth: 200,
|
|
tileHeight: 200,
|
|
outputFormat: "original",
|
|
quality: 90,
|
|
imageDimensions: null,
|
|
processing: false,
|
|
error: null,
|
|
tiles: [],
|
|
zipBlobUrl: null,
|
|
});
|
|
});
|
|
|
|
it("setOutputFormat updates format", () => {
|
|
useSplitStore.getState().setOutputFormat("webp");
|
|
expect(useSplitStore.getState().outputFormat).toBe("webp");
|
|
});
|
|
|
|
it("setQuality updates quality", () => {
|
|
useSplitStore.getState().setQuality(50);
|
|
expect(useSplitStore.getState().quality).toBe(50);
|
|
});
|
|
|
|
it("setProcessing updates processing flag", () => {
|
|
useSplitStore.getState().setProcessing(true);
|
|
expect(useSplitStore.getState().processing).toBe(true);
|
|
});
|
|
|
|
it("setError updates error", () => {
|
|
useSplitStore.getState().setError("Something broke");
|
|
expect(useSplitStore.getState().error).toBe("Something broke");
|
|
});
|
|
|
|
it("setImageDimensions updates dimensions", () => {
|
|
useSplitStore.getState().setImageDimensions({ width: 1920, height: 1080 });
|
|
expect(useSplitStore.getState().imageDimensions).toEqual({ width: 1920, height: 1080 });
|
|
});
|
|
|
|
it("setImageDimensions with null clears dimensions", () => {
|
|
useSplitStore.getState().setImageDimensions({ width: 100, height: 100 });
|
|
useSplitStore.getState().setImageDimensions(null);
|
|
expect(useSplitStore.getState().imageDimensions).toBeNull();
|
|
});
|
|
|
|
it("getEffectiveGrid returns columns/rows in tile-size mode without image dimensions", () => {
|
|
useSplitStore.getState().setMode("tile-size");
|
|
// No image dimensions set, so should fall back to grid mode values
|
|
const grid = useSplitStore.getState().getEffectiveGrid();
|
|
expect(grid).toEqual({ columns: 3, rows: 3 });
|
|
});
|
|
|
|
it("getEffectiveGrid with tile-size computes ceil for non-exact divisions", () => {
|
|
useSplitStore.getState().setMode("tile-size");
|
|
useSplitStore.getState().setTileWidth(150);
|
|
useSplitStore.getState().setTileHeight(200);
|
|
useSplitStore.getState().setImageDimensions({ width: 400, height: 500 });
|
|
const grid = useSplitStore.getState().getEffectiveGrid();
|
|
// 400/150 = 2.67 -> ceil = 3, 500/200 = 2.5 -> ceil = 3
|
|
expect(grid).toEqual({ columns: 3, rows: 3 });
|
|
});
|
|
|
|
it("getComputedTileDimensions in grid mode floors division", () => {
|
|
useSplitStore.getState().setImageDimensions({ width: 1000, height: 700 });
|
|
useSplitStore.getState().setColumns(3);
|
|
useSplitStore.getState().setRows(3);
|
|
const dims = useSplitStore.getState().getComputedTileDimensions();
|
|
// 1000/3 = 333.33 -> floor = 333, 700/3 = 233.33 -> floor = 233
|
|
expect(dims).toEqual({ width: 333, height: 233 });
|
|
});
|
|
|
|
it("setZipBlobUrl with null does not call revokeObjectURL when previous was null", () => {
|
|
revokeObjectURL.mockClear();
|
|
useSplitStore.getState().setZipBlobUrl(null);
|
|
expect(revokeObjectURL).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("setColumns clears tiles on update", () => {
|
|
useSplitStore
|
|
.getState()
|
|
.setTiles([{ row: 0, col: 0, label: "1", width: 100, height: 100, blobUrl: null }]);
|
|
useSplitStore.getState().setColumns(4);
|
|
expect(useSplitStore.getState().tiles).toEqual([]);
|
|
});
|
|
|
|
it("setRows clears tiles on update", () => {
|
|
useSplitStore
|
|
.getState()
|
|
.setTiles([{ row: 0, col: 0, label: "1", width: 100, height: 100, blobUrl: null }]);
|
|
useSplitStore.getState().setRows(5);
|
|
expect(useSplitStore.getState().tiles).toEqual([]);
|
|
});
|
|
|
|
it("setTileWidth clears tiles on update", () => {
|
|
useSplitStore
|
|
.getState()
|
|
.setTiles([{ row: 0, col: 0, label: "1", width: 100, height: 100, blobUrl: null }]);
|
|
useSplitStore.getState().setTileWidth(300);
|
|
expect(useSplitStore.getState().tiles).toEqual([]);
|
|
});
|
|
|
|
it("setTileHeight clears tiles on update", () => {
|
|
useSplitStore
|
|
.getState()
|
|
.setTiles([{ row: 0, col: 0, label: "1", width: 100, height: 100, blobUrl: null }]);
|
|
useSplitStore.getState().setTileHeight(400);
|
|
expect(useSplitStore.getState().tiles).toEqual([]);
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Pipeline Store expanded
|
|
// ---------------------------------------------------------------------------
|
|
import { usePipelineStore } from "@/stores/pipeline-store";
|
|
|
|
describe("usePipelineStore expanded", () => {
|
|
beforeEach(() => {
|
|
usePipelineStore.getState().reset();
|
|
});
|
|
|
|
it("reorderSteps with same ID is effectively a no-op", () => {
|
|
usePipelineStore.getState().addStep("resize");
|
|
const id = usePipelineStore.getState().steps[0].id;
|
|
usePipelineStore.getState().reorderSteps(id, id);
|
|
// Step should still be there
|
|
expect(usePipelineStore.getState().steps).toHaveLength(1);
|
|
expect(usePipelineStore.getState().steps[0].id).toBe(id);
|
|
});
|
|
|
|
it("reorderSteps with one invalid ID is a no-op", () => {
|
|
usePipelineStore.getState().addStep("resize");
|
|
usePipelineStore.getState().addStep("compress");
|
|
const realId = usePipelineStore.getState().steps[0].id;
|
|
usePipelineStore.getState().reorderSteps(realId, "nonexistent");
|
|
// Order unchanged
|
|
expect(usePipelineStore.getState().steps[0].toolId).toBe("resize");
|
|
});
|
|
|
|
it("updateStepSettings for nonexistent ID does not crash", () => {
|
|
usePipelineStore.getState().addStep("resize");
|
|
usePipelineStore.getState().updateStepSettings("nonexistent", { foo: "bar" });
|
|
// Step should be unchanged
|
|
expect(usePipelineStore.getState().steps[0].settings).toEqual({});
|
|
});
|
|
|
|
it("removeStep with nonexistent ID is a no-op", () => {
|
|
usePipelineStore.getState().addStep("resize");
|
|
usePipelineStore.getState().removeStep("nonexistent");
|
|
expect(usePipelineStore.getState().steps).toHaveLength(1);
|
|
});
|
|
|
|
it("loadSteps with empty array clears all steps", () => {
|
|
usePipelineStore.getState().addStep("resize");
|
|
usePipelineStore.getState().addStep("compress");
|
|
usePipelineStore.getState().loadSteps([]);
|
|
expect(usePipelineStore.getState().steps).toEqual([]);
|
|
expect(usePipelineStore.getState().expandedStepId).toBeNull();
|
|
});
|
|
|
|
it("loadSteps creates deep copies of settings", () => {
|
|
const originalSettings = { quality: 80 };
|
|
usePipelineStore.getState().loadSteps([{ toolId: "compress", settings: originalSettings }]);
|
|
// Modifying original should not affect store
|
|
originalSettings.quality = 50;
|
|
expect(usePipelineStore.getState().steps[0].settings).toEqual({ quality: 80 });
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Duplicate Store expanded
|
|
// ---------------------------------------------------------------------------
|
|
import { useDuplicateStore } from "@/stores/duplicate-store";
|
|
|
|
describe("useDuplicateStore expanded", () => {
|
|
beforeEach(() => {
|
|
useDuplicateStore.getState().reset();
|
|
});
|
|
|
|
it("overrideBest then reset clears all overrides", () => {
|
|
useDuplicateStore.getState().overrideBest(0, 2);
|
|
useDuplicateStore.getState().overrideBest(1, 3);
|
|
useDuplicateStore.getState().reset();
|
|
expect(useDuplicateStore.getState().bestOverrides).toEqual({});
|
|
});
|
|
|
|
it("setViewMode to overview preserves selectedGroupIndex", () => {
|
|
useDuplicateStore.getState().setSelectedGroup(5);
|
|
useDuplicateStore.getState().setViewMode("overview");
|
|
expect(useDuplicateStore.getState().viewMode).toBe("overview");
|
|
expect(useDuplicateStore.getState().selectedGroupIndex).toBe(5);
|
|
});
|
|
|
|
it("setScanning to false preserves other state", () => {
|
|
useDuplicateStore.getState().setScanning(true);
|
|
useDuplicateStore.getState().setViewMode("detail");
|
|
useDuplicateStore.getState().setScanning(false);
|
|
expect(useDuplicateStore.getState().scanning).toBe(false);
|
|
expect(useDuplicateStore.getState().viewMode).toBe("detail");
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Base64 Store expanded
|
|
// ---------------------------------------------------------------------------
|
|
import type { Base64Result } from "@/stores/base64-store";
|
|
import { useBase64Store } from "@/stores/base64-store";
|
|
|
|
describe("useBase64Store expanded", () => {
|
|
beforeEach(() => {
|
|
useBase64Store.getState().reset();
|
|
});
|
|
|
|
it("addResult followed by addError does not clear results", () => {
|
|
const r: Base64Result = {
|
|
filename: "ok.png",
|
|
mimeType: "image/png",
|
|
width: 10,
|
|
height: 10,
|
|
originalSize: 100,
|
|
encodedSize: 133,
|
|
overheadPercent: 33,
|
|
base64: "x",
|
|
dataUri: "data:image/png;base64,x",
|
|
};
|
|
useBase64Store.getState().addResult(r);
|
|
useBase64Store.getState().addError({ filename: "bad.png", error: "fail" });
|
|
expect(useBase64Store.getState().results).toHaveLength(1);
|
|
expect(useBase64Store.getState().errors).toHaveLength(1);
|
|
});
|
|
|
|
it("setResults replaces previous results entirely", () => {
|
|
const r1: Base64Result = {
|
|
filename: "a.png",
|
|
mimeType: "image/png",
|
|
width: 1,
|
|
height: 1,
|
|
originalSize: 10,
|
|
encodedSize: 13,
|
|
overheadPercent: 30,
|
|
base64: "a",
|
|
dataUri: "data:image/png;base64,a",
|
|
};
|
|
useBase64Store.getState().addResult(r1);
|
|
useBase64Store.getState().addResult({ ...r1, filename: "b.png" });
|
|
|
|
const newResults: Base64Result[] = [{ ...r1, filename: "c.png" }];
|
|
useBase64Store.getState().setResults(newResults, []);
|
|
expect(useBase64Store.getState().results).toHaveLength(1);
|
|
expect(useBase64Store.getState().results[0].filename).toBe("c.png");
|
|
});
|
|
|
|
it("setProgress with valid progress then null clears progress", () => {
|
|
useBase64Store.getState().setProgress({ completed: 1, total: 3, currentFile: "a.png" });
|
|
expect(useBase64Store.getState().progress).not.toBeNull();
|
|
useBase64Store.getState().setProgress(null);
|
|
expect(useBase64Store.getState().progress).toBeNull();
|
|
});
|
|
});
|