Files
SnapOtter/tests/unit/web/stores-expanded.test.ts
T
SnapOtter 06d1822491 test: expand test coverage across all layers (+1,157 tests)
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
2026-06-06 19:37:29 +08:00

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();
});
});