mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
feat: rewrite file-store with FileEntry model for multi-image support
This commit is contained in:
@@ -1,83 +1,258 @@
|
|||||||
import { create } from "zustand";
|
import { create } from "zustand";
|
||||||
|
|
||||||
interface FileState {
|
export interface FileEntry {
|
||||||
files: File[];
|
file: File;
|
||||||
jobId: string | null;
|
blobUrl: string;
|
||||||
processedUrl: string | null;
|
processedUrl: string | null;
|
||||||
/** Blob URL for the original image (for before/after comparison). */
|
processedSize: number | null;
|
||||||
originalBlobUrl: string | null;
|
originalSize: number;
|
||||||
|
status: "pending" | "processing" | "completed" | "failed";
|
||||||
|
error: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Helpers
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
function createEntry(file: File): FileEntry {
|
||||||
|
return {
|
||||||
|
file,
|
||||||
|
blobUrl: URL.createObjectURL(file),
|
||||||
|
processedUrl: null,
|
||||||
|
processedSize: null,
|
||||||
|
originalSize: file.size,
|
||||||
|
status: "pending",
|
||||||
|
error: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function revokeEntries(entries: FileEntry[]): void {
|
||||||
|
for (const entry of entries) {
|
||||||
|
URL.revokeObjectURL(entry.blobUrl);
|
||||||
|
if (entry.processedUrl) URL.revokeObjectURL(entry.processedUrl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Store
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
interface FileState {
|
||||||
|
entries: FileEntry[];
|
||||||
|
selectedIndex: number;
|
||||||
|
batchZipBlob: Blob | null;
|
||||||
|
batchZipFilename: string | null;
|
||||||
processing: boolean;
|
processing: boolean;
|
||||||
error: string | null;
|
error: string | null;
|
||||||
originalSize: number | null;
|
|
||||||
processedSize: number | null;
|
// Backward compat getters (computed from entries + selectedIndex)
|
||||||
selectedFileName: string | null;
|
readonly files: File[];
|
||||||
selectedFileSize: number | null;
|
readonly currentEntry: FileEntry | undefined;
|
||||||
|
readonly hasFiles: boolean;
|
||||||
|
readonly allProcessed: boolean;
|
||||||
|
readonly selectedFileName: string | null;
|
||||||
|
readonly selectedFileSize: number | null;
|
||||||
|
readonly originalBlobUrl: string | null;
|
||||||
|
readonly processedUrl: string | null;
|
||||||
|
readonly originalSize: number | null;
|
||||||
|
readonly processedSize: number | null;
|
||||||
|
|
||||||
|
// Actions
|
||||||
setFiles: (files: File[]) => void;
|
setFiles: (files: File[]) => void;
|
||||||
setJobId: (id: string) => void;
|
addFiles: (files: File[]) => void;
|
||||||
setProcessedUrl: (url: string | null) => void;
|
removeFile: (index: number) => void;
|
||||||
|
setSelectedIndex: (index: number) => void;
|
||||||
|
navigateNext: () => void;
|
||||||
|
navigatePrev: () => void;
|
||||||
|
updateEntry: (index: number, patch: Partial<FileEntry>) => void;
|
||||||
|
setBatchZip: (blob: Blob, filename: string) => void;
|
||||||
setProcessing: (v: boolean) => void;
|
setProcessing: (v: boolean) => void;
|
||||||
setError: (e: string | null) => void;
|
setError: (e: string | null) => void;
|
||||||
|
setJobId: (id: string) => void;
|
||||||
|
setProcessedUrl: (url: string | null) => void;
|
||||||
setSizes: (original: number, processed: number) => void;
|
setSizes: (original: number, processed: number) => void;
|
||||||
/** Clear processed result but keep the original uploaded file. */
|
|
||||||
undoProcessing: () => void;
|
undoProcessing: () => void;
|
||||||
reset: () => void;
|
reset: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compute backward-compat derived values from core state.
|
||||||
|
* Called after every state mutation to keep derived fields in sync.
|
||||||
|
*/
|
||||||
|
function deriveCompat(entries: FileEntry[], selectedIndex: number) {
|
||||||
|
const entry = entries[selectedIndex];
|
||||||
|
return {
|
||||||
|
files: entries.map((e) => e.file),
|
||||||
|
currentEntry: entry,
|
||||||
|
hasFiles: entries.length > 0,
|
||||||
|
allProcessed:
|
||||||
|
entries.length > 0 && entries.every((e) => e.status === "completed"),
|
||||||
|
selectedFileName: entry ? entry.file.name : null,
|
||||||
|
selectedFileSize: entry ? entry.file.size : null,
|
||||||
|
originalBlobUrl: entry ? entry.blobUrl : null,
|
||||||
|
processedUrl: entry ? entry.processedUrl : null,
|
||||||
|
originalSize: entry ? entry.originalSize : null,
|
||||||
|
processedSize: entry ? entry.processedSize : null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export const useFileStore = create<FileState>((set, get) => ({
|
export const useFileStore = create<FileState>((set, get) => ({
|
||||||
files: [],
|
entries: [],
|
||||||
jobId: null,
|
selectedIndex: 0,
|
||||||
processedUrl: null,
|
batchZipBlob: null,
|
||||||
originalBlobUrl: null,
|
batchZipFilename: null,
|
||||||
processing: false,
|
processing: false,
|
||||||
error: null,
|
error: null,
|
||||||
originalSize: null,
|
|
||||||
processedSize: null,
|
// Initial derived values (empty state)
|
||||||
selectedFileName: null,
|
...deriveCompat([], 0),
|
||||||
selectedFileSize: null,
|
|
||||||
|
// -- Actions --------------------------------------------------------------
|
||||||
|
|
||||||
setFiles: (files) => {
|
setFiles: (files) => {
|
||||||
// Revoke old blob URL if any
|
revokeEntries(get().entries);
|
||||||
const old = get().originalBlobUrl;
|
const entries = files.map(createEntry);
|
||||||
if (old) URL.revokeObjectURL(old);
|
|
||||||
// Create a blob URL for the first file for before/after preview
|
|
||||||
const blobUrl = files.length > 0 ? URL.createObjectURL(files[0]) : null;
|
|
||||||
const firstName = files.length > 0 ? files[0].name : null;
|
|
||||||
const firstSize = files.length > 0 ? files[0].size : null;
|
|
||||||
set({
|
set({
|
||||||
files,
|
entries,
|
||||||
|
selectedIndex: 0,
|
||||||
error: null,
|
error: null,
|
||||||
originalBlobUrl: blobUrl,
|
...deriveCompat(entries, 0),
|
||||||
selectedFileName: firstName,
|
|
||||||
selectedFileSize: firstSize,
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
setJobId: (id) => set({ jobId: id }),
|
|
||||||
setProcessedUrl: (url) => set({ processedUrl: url }),
|
addFiles: (files) => {
|
||||||
|
const entries = [...get().entries, ...files.map(createEntry)];
|
||||||
|
const idx = get().selectedIndex;
|
||||||
|
set({ entries, ...deriveCompat(entries, idx) });
|
||||||
|
},
|
||||||
|
|
||||||
|
removeFile: (index) => {
|
||||||
|
const { entries, selectedIndex } = get();
|
||||||
|
const removed = entries[index];
|
||||||
|
if (!removed) return;
|
||||||
|
|
||||||
|
URL.revokeObjectURL(removed.blobUrl);
|
||||||
|
if (removed.processedUrl) URL.revokeObjectURL(removed.processedUrl);
|
||||||
|
|
||||||
|
const newEntries = entries.filter((_, i) => i !== index);
|
||||||
|
let newIndex = selectedIndex;
|
||||||
|
if (index < selectedIndex) {
|
||||||
|
newIndex = selectedIndex - 1;
|
||||||
|
} else if (selectedIndex >= newEntries.length && newEntries.length > 0) {
|
||||||
|
newIndex = newEntries.length - 1;
|
||||||
|
} else if (newEntries.length === 0) {
|
||||||
|
newIndex = 0;
|
||||||
|
}
|
||||||
|
set({
|
||||||
|
entries: newEntries,
|
||||||
|
selectedIndex: newIndex,
|
||||||
|
...deriveCompat(newEntries, newIndex),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
setSelectedIndex: (index) => {
|
||||||
|
set({
|
||||||
|
selectedIndex: index,
|
||||||
|
...deriveCompat(get().entries, index),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
navigateNext: () => {
|
||||||
|
const { selectedIndex, entries } = get();
|
||||||
|
if (selectedIndex < entries.length - 1) {
|
||||||
|
const idx = selectedIndex + 1;
|
||||||
|
set({ selectedIndex: idx, ...deriveCompat(entries, idx) });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
navigatePrev: () => {
|
||||||
|
const { selectedIndex, entries } = get();
|
||||||
|
if (selectedIndex > 0) {
|
||||||
|
const idx = selectedIndex - 1;
|
||||||
|
set({ selectedIndex: idx, ...deriveCompat(entries, idx) });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
updateEntry: (index, patch) => {
|
||||||
|
const entries = [...get().entries];
|
||||||
|
if (!entries[index]) return;
|
||||||
|
entries[index] = { ...entries[index], ...patch };
|
||||||
|
const idx = get().selectedIndex;
|
||||||
|
set({ entries, ...deriveCompat(entries, idx) });
|
||||||
|
},
|
||||||
|
|
||||||
|
setBatchZip: (blob, filename) =>
|
||||||
|
set({ batchZipBlob: blob, batchZipFilename: filename }),
|
||||||
|
|
||||||
setProcessing: (v) => set({ processing: v }),
|
setProcessing: (v) => set({ processing: v }),
|
||||||
|
|
||||||
setError: (e) => set({ error: e, processing: false }),
|
setError: (e) => set({ error: e, processing: false }),
|
||||||
setSizes: (original, processed) =>
|
|
||||||
set({ originalSize: original, processedSize: processed }),
|
setJobId: (_id) => {
|
||||||
|
// no-op for backward compat
|
||||||
|
},
|
||||||
|
|
||||||
|
setProcessedUrl: (url) => {
|
||||||
|
const { entries, selectedIndex } = get();
|
||||||
|
if (!entries[selectedIndex]) return;
|
||||||
|
const updated = [...entries];
|
||||||
|
if (url) {
|
||||||
|
updated[selectedIndex] = {
|
||||||
|
...updated[selectedIndex],
|
||||||
|
processedUrl: url,
|
||||||
|
status: "completed",
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
updated[selectedIndex] = {
|
||||||
|
...updated[selectedIndex],
|
||||||
|
processedUrl: null,
|
||||||
|
status: "pending",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
set({ entries: updated, ...deriveCompat(updated, selectedIndex) });
|
||||||
|
},
|
||||||
|
|
||||||
|
setSizes: (original, processed) => {
|
||||||
|
const { entries, selectedIndex } = get();
|
||||||
|
if (!entries[selectedIndex]) return;
|
||||||
|
const updated = [...entries];
|
||||||
|
updated[selectedIndex] = {
|
||||||
|
...updated[selectedIndex],
|
||||||
|
originalSize: original,
|
||||||
|
processedSize: processed,
|
||||||
|
};
|
||||||
|
set({ entries: updated, ...deriveCompat(updated, selectedIndex) });
|
||||||
|
},
|
||||||
|
|
||||||
undoProcessing: () => {
|
undoProcessing: () => {
|
||||||
set({
|
const { entries, selectedIndex } = get();
|
||||||
|
for (const entry of entries) {
|
||||||
|
if (entry.processedUrl) URL.revokeObjectURL(entry.processedUrl);
|
||||||
|
}
|
||||||
|
const resetEntries = entries.map((e) => ({
|
||||||
|
...e,
|
||||||
processedUrl: null,
|
processedUrl: null,
|
||||||
jobId: null,
|
|
||||||
processedSize: null,
|
processedSize: null,
|
||||||
|
status: "pending" as const,
|
||||||
error: null,
|
error: null,
|
||||||
|
}));
|
||||||
|
set({
|
||||||
|
entries: resetEntries,
|
||||||
|
error: null,
|
||||||
|
...deriveCompat(resetEntries, selectedIndex),
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
reset: () => {
|
reset: () => {
|
||||||
const old = get().originalBlobUrl;
|
revokeEntries(get().entries);
|
||||||
if (old) URL.revokeObjectURL(old);
|
|
||||||
set({
|
set({
|
||||||
files: [],
|
entries: [],
|
||||||
jobId: null,
|
selectedIndex: 0,
|
||||||
processedUrl: null,
|
batchZipBlob: null,
|
||||||
originalBlobUrl: null,
|
batchZipFilename: null,
|
||||||
processing: false,
|
processing: false,
|
||||||
error: null,
|
error: null,
|
||||||
originalSize: null,
|
...deriveCompat([], 0),
|
||||||
processedSize: null,
|
|
||||||
selectedFileName: null,
|
|
||||||
selectedFileSize: null,
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|||||||
+327
-219
@@ -82,295 +82,403 @@ function failResponse(status: number) {
|
|||||||
|
|
||||||
describe("FileStore", () => {
|
describe("FileStore", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
// Reset the store to initial state before every test.
|
|
||||||
// Zustand keeps state across calls, so we manually reset.
|
|
||||||
useFileStore.getState().reset();
|
useFileStore.getState().reset();
|
||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
// After reset, createObjectURL/revokeObjectURL calls are from reset itself;
|
|
||||||
// clear them so each test starts clean.
|
|
||||||
createObjectURL.mockClear();
|
createObjectURL.mockClear();
|
||||||
revokeObjectURL.mockClear();
|
revokeObjectURL.mockClear();
|
||||||
|
// Reset the mock to return incrementing URLs
|
||||||
|
let urlCounter = 0;
|
||||||
|
createObjectURL.mockImplementation(
|
||||||
|
(_obj: Blob | MediaSource) => `blob:url-${++urlCounter}`,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
// -- Initial state -------------------------------------------------------
|
// -- Initial state -------------------------------------------------------
|
||||||
|
|
||||||
it("has correct initial state (everything null/empty/false)", () => {
|
it("has correct initial state", () => {
|
||||||
const s = useFileStore.getState();
|
const s = useFileStore.getState();
|
||||||
expect(s.files).toEqual([]);
|
expect(s.entries).toEqual([]);
|
||||||
expect(s.jobId).toBeNull();
|
expect(s.selectedIndex).toBe(0);
|
||||||
expect(s.processedUrl).toBeNull();
|
expect(s.batchZipBlob).toBeNull();
|
||||||
expect(s.originalBlobUrl).toBeNull();
|
expect(s.batchZipFilename).toBeNull();
|
||||||
expect(s.processing).toBe(false);
|
expect(s.processing).toBe(false);
|
||||||
expect(s.error).toBeNull();
|
expect(s.error).toBeNull();
|
||||||
expect(s.originalSize).toBeNull();
|
|
||||||
expect(s.processedSize).toBeNull();
|
|
||||||
expect(s.selectedFileName).toBeNull();
|
|
||||||
expect(s.selectedFileSize).toBeNull();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// -- setFiles -------------------------------------------------------------
|
// -- setFiles -------------------------------------------------------------
|
||||||
|
|
||||||
it("setFiles stores files, creates blob URL, sets selectedFileName/Size, clears error", () => {
|
it("setFiles creates entries with blob URLs", () => {
|
||||||
// Seed an error first so we can verify it gets cleared
|
const f1 = makeFile("a.png", 100);
|
||||||
useFileStore.getState().setError("old error");
|
const f2 = makeFile("b.png", 200);
|
||||||
expect(useFileStore.getState().error).toBe("old error");
|
|
||||||
|
|
||||||
const file = makeFile("photo.png", 2048);
|
|
||||||
useFileStore.getState().setFiles([file]);
|
|
||||||
|
|
||||||
const s = useFileStore.getState();
|
|
||||||
expect(s.files).toHaveLength(1);
|
|
||||||
expect(s.files[0]).toBe(file);
|
|
||||||
expect(createObjectURL).toHaveBeenCalledWith(file);
|
|
||||||
expect(s.originalBlobUrl).toBe("blob:fake-url");
|
|
||||||
expect(s.selectedFileName).toBe("photo.png");
|
|
||||||
expect(s.selectedFileSize).toBe(2048);
|
|
||||||
expect(s.error).toBeNull(); // error cleared
|
|
||||||
});
|
|
||||||
|
|
||||||
it("setFiles revokes the previous blob URL when new files are set", () => {
|
|
||||||
createObjectURL
|
|
||||||
.mockReturnValueOnce("blob:first-url")
|
|
||||||
.mockReturnValueOnce("blob:second-url");
|
|
||||||
|
|
||||||
useFileStore.getState().setFiles([makeFile("a.png")]);
|
|
||||||
expect(useFileStore.getState().originalBlobUrl).toBe("blob:first-url");
|
|
||||||
|
|
||||||
useFileStore.getState().setFiles([makeFile("b.png")]);
|
|
||||||
expect(revokeObjectURL).toHaveBeenCalledWith("blob:first-url");
|
|
||||||
expect(useFileStore.getState().originalBlobUrl).toBe("blob:second-url");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("setFiles with empty array does NOT create a blob URL", () => {
|
|
||||||
useFileStore.getState().setFiles([]);
|
|
||||||
|
|
||||||
const s = useFileStore.getState();
|
|
||||||
expect(createObjectURL).not.toHaveBeenCalled();
|
|
||||||
expect(s.originalBlobUrl).toBeNull();
|
|
||||||
expect(s.selectedFileName).toBeNull();
|
|
||||||
expect(s.selectedFileSize).toBeNull();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("setFiles with empty array after prior files still revokes old URL", () => {
|
|
||||||
createObjectURL.mockReturnValueOnce("blob:old");
|
|
||||||
useFileStore.getState().setFiles([makeFile("old.png")]);
|
|
||||||
revokeObjectURL.mockClear();
|
|
||||||
|
|
||||||
useFileStore.getState().setFiles([]);
|
|
||||||
expect(revokeObjectURL).toHaveBeenCalledWith("blob:old");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("setFiles uses the FIRST file for blob URL when given multiple files", () => {
|
|
||||||
const f1 = makeFile("first.png", 100);
|
|
||||||
const f2 = makeFile("second.png", 200);
|
|
||||||
useFileStore.getState().setFiles([f1, f2]);
|
useFileStore.getState().setFiles([f1, f2]);
|
||||||
|
|
||||||
// createObjectURL is called exactly once (only for the first file)
|
|
||||||
expect(createObjectURL).toHaveBeenCalledTimes(1);
|
|
||||||
// Verify the argument was f1 by identity (same reference)
|
|
||||||
expect(createObjectURL.mock.calls[0][0]).toBe(f1);
|
|
||||||
expect(useFileStore.getState().selectedFileName).toBe("first.png");
|
|
||||||
expect(useFileStore.getState().selectedFileSize).toBe(100);
|
|
||||||
});
|
|
||||||
|
|
||||||
// -- setJobId -------------------------------------------------------------
|
|
||||||
|
|
||||||
it("setJobId stores the job ID", () => {
|
|
||||||
useFileStore.getState().setJobId("job-abc");
|
|
||||||
expect(useFileStore.getState().jobId).toBe("job-abc");
|
|
||||||
});
|
|
||||||
|
|
||||||
// -- setProcessedUrl ------------------------------------------------------
|
|
||||||
|
|
||||||
it("setProcessedUrl stores a URL", () => {
|
|
||||||
useFileStore.getState().setProcessedUrl("blob:processed");
|
|
||||||
expect(useFileStore.getState().processedUrl).toBe("blob:processed");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("setProcessedUrl can clear URL with null", () => {
|
|
||||||
useFileStore.getState().setProcessedUrl("blob:x");
|
|
||||||
useFileStore.getState().setProcessedUrl(null);
|
|
||||||
expect(useFileStore.getState().processedUrl).toBeNull();
|
|
||||||
});
|
|
||||||
|
|
||||||
// -- setProcessing --------------------------------------------------------
|
|
||||||
|
|
||||||
it("setProcessing sets the processing flag", () => {
|
|
||||||
useFileStore.getState().setProcessing(true);
|
|
||||||
expect(useFileStore.getState().processing).toBe(true);
|
|
||||||
useFileStore.getState().setProcessing(false);
|
|
||||||
expect(useFileStore.getState().processing).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
// -- setError -------------------------------------------------------------
|
|
||||||
|
|
||||||
it("setError sets error AND forces processing to false", () => {
|
|
||||||
useFileStore.getState().setProcessing(true);
|
|
||||||
expect(useFileStore.getState().processing).toBe(true);
|
|
||||||
|
|
||||||
useFileStore.getState().setError("something broke");
|
|
||||||
const s = useFileStore.getState();
|
const s = useFileStore.getState();
|
||||||
expect(s.error).toBe("something broke");
|
expect(s.entries).toHaveLength(2);
|
||||||
expect(s.processing).toBe(false); // critical side-effect
|
expect(s.entries[0].file).toBe(f1);
|
||||||
|
expect(s.entries[0].blobUrl).toBe("blob:url-1");
|
||||||
|
expect(s.entries[0].originalSize).toBe(100);
|
||||||
|
expect(s.entries[0].status).toBe("pending");
|
||||||
|
expect(s.entries[0].processedUrl).toBeNull();
|
||||||
|
expect(s.entries[0].processedSize).toBeNull();
|
||||||
|
expect(s.entries[0].error).toBeNull();
|
||||||
|
expect(s.entries[1].file).toBe(f2);
|
||||||
|
expect(s.entries[1].blobUrl).toBe("blob:url-2");
|
||||||
|
expect(createObjectURL).toHaveBeenCalledTimes(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("setError(null) clears error but still forces processing to false", () => {
|
it("setFiles revokes old blob URLs", () => {
|
||||||
useFileStore.getState().setProcessing(true);
|
useFileStore.getState().setFiles([makeFile("a.png")]);
|
||||||
useFileStore.getState().setError(null);
|
const oldUrl = useFileStore.getState().entries[0].blobUrl;
|
||||||
|
revokeObjectURL.mockClear();
|
||||||
|
|
||||||
|
useFileStore.getState().setFiles([makeFile("b.png")]);
|
||||||
|
expect(revokeObjectURL).toHaveBeenCalledWith(oldUrl);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("setFiles clears on empty array", () => {
|
||||||
|
useFileStore.getState().setFiles([makeFile("a.png")]);
|
||||||
|
revokeObjectURL.mockClear();
|
||||||
|
const oldUrl = useFileStore.getState().entries[0].blobUrl;
|
||||||
|
|
||||||
|
useFileStore.getState().setFiles([]);
|
||||||
|
expect(useFileStore.getState().entries).toEqual([]);
|
||||||
|
expect(revokeObjectURL).toHaveBeenCalledWith(oldUrl);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("setFiles resets selectedIndex to 0", () => {
|
||||||
|
useFileStore.getState().setFiles([makeFile("a.png"), makeFile("b.png")]);
|
||||||
|
useFileStore.getState().setSelectedIndex(1);
|
||||||
|
expect(useFileStore.getState().selectedIndex).toBe(1);
|
||||||
|
|
||||||
|
useFileStore.getState().setFiles([makeFile("c.png")]);
|
||||||
|
expect(useFileStore.getState().selectedIndex).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("setFiles clears error", () => {
|
||||||
|
useFileStore.getState().setError("old error");
|
||||||
|
useFileStore.getState().setFiles([makeFile("a.png")]);
|
||||||
expect(useFileStore.getState().error).toBeNull();
|
expect(useFileStore.getState().error).toBeNull();
|
||||||
expect(useFileStore.getState().processing).toBe(false);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// -- setSizes -------------------------------------------------------------
|
// -- addFiles -------------------------------------------------------------
|
||||||
|
|
||||||
|
it("addFiles appends new entries without revoking existing", () => {
|
||||||
|
useFileStore.getState().setFiles([makeFile("a.png", 100)]);
|
||||||
|
revokeObjectURL.mockClear();
|
||||||
|
createObjectURL.mockClear();
|
||||||
|
|
||||||
|
const f2 = makeFile("b.png", 200);
|
||||||
|
useFileStore.getState().addFiles([f2]);
|
||||||
|
|
||||||
|
expect(revokeObjectURL).not.toHaveBeenCalled();
|
||||||
|
expect(useFileStore.getState().entries).toHaveLength(2);
|
||||||
|
expect(useFileStore.getState().entries[1].file).toBe(f2);
|
||||||
|
expect(createObjectURL).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
// -- removeFile -----------------------------------------------------------
|
||||||
|
|
||||||
|
it("removeFile removes entry and revokes its blob URLs", () => {
|
||||||
|
useFileStore.getState().setFiles([makeFile("a.png"), makeFile("b.png")]);
|
||||||
|
const removedUrl = useFileStore.getState().entries[0].blobUrl;
|
||||||
|
revokeObjectURL.mockClear();
|
||||||
|
|
||||||
|
useFileStore.getState().removeFile(0);
|
||||||
|
expect(useFileStore.getState().entries).toHaveLength(1);
|
||||||
|
expect(useFileStore.getState().entries[0].file.name).toBe("b.png");
|
||||||
|
expect(revokeObjectURL).toHaveBeenCalledWith(removedUrl);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("removeFile adjusts selectedIndex when removing before it", () => {
|
||||||
|
useFileStore.getState().setFiles([
|
||||||
|
makeFile("a.png"),
|
||||||
|
makeFile("b.png"),
|
||||||
|
makeFile("c.png"),
|
||||||
|
]);
|
||||||
|
useFileStore.getState().setSelectedIndex(2);
|
||||||
|
|
||||||
|
useFileStore.getState().removeFile(0);
|
||||||
|
expect(useFileStore.getState().selectedIndex).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("removeFile clamps selectedIndex if it was the last entry", () => {
|
||||||
|
useFileStore.getState().setFiles([makeFile("a.png"), makeFile("b.png")]);
|
||||||
|
useFileStore.getState().setSelectedIndex(1);
|
||||||
|
|
||||||
|
useFileStore.getState().removeFile(1);
|
||||||
|
expect(useFileStore.getState().selectedIndex).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("removeFile revokes processedUrl if present", () => {
|
||||||
|
useFileStore.getState().setFiles([makeFile("a.png")]);
|
||||||
|
useFileStore.getState().updateEntry(0, {
|
||||||
|
processedUrl: "blob:processed",
|
||||||
|
status: "completed",
|
||||||
|
});
|
||||||
|
revokeObjectURL.mockClear();
|
||||||
|
|
||||||
|
useFileStore.getState().removeFile(0);
|
||||||
|
expect(revokeObjectURL).toHaveBeenCalledWith("blob:processed");
|
||||||
|
});
|
||||||
|
|
||||||
|
// -- Navigation -----------------------------------------------------------
|
||||||
|
|
||||||
|
it("navigateNext advances selectedIndex", () => {
|
||||||
|
useFileStore.getState().setFiles([makeFile("a.png"), makeFile("b.png")]);
|
||||||
|
expect(useFileStore.getState().selectedIndex).toBe(0);
|
||||||
|
|
||||||
|
useFileStore.getState().navigateNext();
|
||||||
|
expect(useFileStore.getState().selectedIndex).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("navigateNext does not exceed bounds", () => {
|
||||||
|
useFileStore.getState().setFiles([makeFile("a.png"), makeFile("b.png")]);
|
||||||
|
useFileStore.getState().setSelectedIndex(1);
|
||||||
|
|
||||||
|
useFileStore.getState().navigateNext();
|
||||||
|
expect(useFileStore.getState().selectedIndex).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("navigatePrev decrements selectedIndex", () => {
|
||||||
|
useFileStore.getState().setFiles([makeFile("a.png"), makeFile("b.png")]);
|
||||||
|
useFileStore.getState().setSelectedIndex(1);
|
||||||
|
|
||||||
|
useFileStore.getState().navigatePrev();
|
||||||
|
expect(useFileStore.getState().selectedIndex).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("navigatePrev does not go below 0", () => {
|
||||||
|
useFileStore.getState().setFiles([makeFile("a.png")]);
|
||||||
|
useFileStore.getState().navigatePrev();
|
||||||
|
expect(useFileStore.getState().selectedIndex).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
// -- updateEntry ----------------------------------------------------------
|
||||||
|
|
||||||
|
it("updateEntry merges partial data into the entry at index", () => {
|
||||||
|
useFileStore.getState().setFiles([makeFile("a.png", 500)]);
|
||||||
|
useFileStore.getState().updateEntry(0, {
|
||||||
|
status: "completed",
|
||||||
|
processedUrl: "blob:done",
|
||||||
|
processedSize: 250,
|
||||||
|
});
|
||||||
|
|
||||||
|
const entry = useFileStore.getState().entries[0];
|
||||||
|
expect(entry.status).toBe("completed");
|
||||||
|
expect(entry.processedUrl).toBe("blob:done");
|
||||||
|
expect(entry.processedSize).toBe(250);
|
||||||
|
expect(entry.file.name).toBe("a.png"); // unchanged
|
||||||
|
});
|
||||||
|
|
||||||
|
// -- setBatchZip ----------------------------------------------------------
|
||||||
|
|
||||||
|
it("setBatchZip stores blob and filename", () => {
|
||||||
|
const blob = new Blob(["zip-data"]);
|
||||||
|
useFileStore.getState().setBatchZip(blob, "results.zip");
|
||||||
|
|
||||||
it("setSizes sets both originalSize and processedSize", () => {
|
|
||||||
useFileStore.getState().setSizes(5000, 2500);
|
|
||||||
const s = useFileStore.getState();
|
const s = useFileStore.getState();
|
||||||
expect(s.originalSize).toBe(5000);
|
expect(s.batchZipBlob).toBe(blob);
|
||||||
expect(s.processedSize).toBe(2500);
|
expect(s.batchZipFilename).toBe("results.zip");
|
||||||
});
|
|
||||||
|
|
||||||
it("setSizes with zero values stores zeros (not null)", () => {
|
|
||||||
useFileStore.getState().setSizes(0, 0);
|
|
||||||
expect(useFileStore.getState().originalSize).toBe(0);
|
|
||||||
expect(useFileStore.getState().processedSize).toBe(0);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// -- undoProcessing -------------------------------------------------------
|
// -- undoProcessing -------------------------------------------------------
|
||||||
|
|
||||||
it("undoProcessing clears processedUrl, jobId, processedSize, error but KEEPS files and originalBlobUrl", () => {
|
it("undoProcessing resets all entries to pending and revokes processed blob URLs", () => {
|
||||||
createObjectURL.mockReturnValueOnce("blob:orig");
|
useFileStore.getState().setFiles([makeFile("a.png"), makeFile("b.png")]);
|
||||||
|
useFileStore.getState().updateEntry(0, {
|
||||||
// Set up full state
|
status: "completed",
|
||||||
const file = makeFile("keep-me.png", 3000);
|
processedUrl: "blob:proc-a",
|
||||||
useFileStore.getState().setFiles([file]);
|
processedSize: 50,
|
||||||
useFileStore.getState().setJobId("job-1");
|
});
|
||||||
useFileStore.getState().setProcessedUrl("blob:result");
|
useFileStore.getState().updateEntry(1, {
|
||||||
useFileStore.getState().setSizes(3000, 1500);
|
status: "completed",
|
||||||
useFileStore.getState().setError("transient error");
|
processedUrl: "blob:proc-b",
|
||||||
|
processedSize: 75,
|
||||||
|
});
|
||||||
|
revokeObjectURL.mockClear();
|
||||||
|
|
||||||
useFileStore.getState().undoProcessing();
|
useFileStore.getState().undoProcessing();
|
||||||
|
|
||||||
const s = useFileStore.getState();
|
const s = useFileStore.getState();
|
||||||
// Cleared
|
// All entries reset to pending
|
||||||
expect(s.processedUrl).toBeNull();
|
expect(s.entries[0].status).toBe("pending");
|
||||||
expect(s.jobId).toBeNull();
|
expect(s.entries[0].processedUrl).toBeNull();
|
||||||
expect(s.processedSize).toBeNull();
|
expect(s.entries[0].processedSize).toBeNull();
|
||||||
expect(s.error).toBeNull();
|
expect(s.entries[0].error).toBeNull();
|
||||||
// Preserved
|
expect(s.entries[1].status).toBe("pending");
|
||||||
expect(s.files).toHaveLength(1);
|
expect(s.entries[1].processedUrl).toBeNull();
|
||||||
expect(s.files[0]).toBe(file);
|
// Processed URLs revoked
|
||||||
expect(s.originalBlobUrl).toBe("blob:orig");
|
expect(revokeObjectURL).toHaveBeenCalledWith("blob:proc-a");
|
||||||
expect(s.selectedFileName).toBe("keep-me.png");
|
expect(revokeObjectURL).toHaveBeenCalledWith("blob:proc-b");
|
||||||
expect(s.selectedFileSize).toBe(3000);
|
|
||||||
// originalSize is NOT cleared by undoProcessing (only processedSize is)
|
|
||||||
expect(s.originalSize).toBe(3000);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("undoProcessing does NOT revoke the originalBlobUrl", () => {
|
it("undoProcessing keeps original blob URLs", () => {
|
||||||
createObjectURL.mockReturnValueOnce("blob:keep-alive");
|
useFileStore.getState().setFiles([makeFile("a.png")]);
|
||||||
useFileStore.getState().setFiles([makeFile("x.png")]);
|
const origUrl = useFileStore.getState().entries[0].blobUrl;
|
||||||
revokeObjectURL.mockClear();
|
revokeObjectURL.mockClear();
|
||||||
|
|
||||||
useFileStore.getState().undoProcessing();
|
useFileStore.getState().undoProcessing();
|
||||||
expect(revokeObjectURL).not.toHaveBeenCalled();
|
// Should NOT revoke original blob URL
|
||||||
|
expect(revokeObjectURL).not.toHaveBeenCalledWith(origUrl);
|
||||||
|
expect(useFileStore.getState().entries[0].blobUrl).toBe(origUrl);
|
||||||
});
|
});
|
||||||
|
|
||||||
// -- reset ----------------------------------------------------------------
|
// -- reset ----------------------------------------------------------------
|
||||||
|
|
||||||
it("reset clears everything and revokes the blob URL", () => {
|
it("reset clears everything and revokes all blob URLs", () => {
|
||||||
createObjectURL.mockReturnValueOnce("blob:to-revoke");
|
useFileStore.getState().setFiles([makeFile("a.png"), makeFile("b.png")]);
|
||||||
useFileStore.getState().setFiles([makeFile("doomed.png")]);
|
useFileStore.getState().updateEntry(0, { processedUrl: "blob:proc" });
|
||||||
useFileStore.getState().setJobId("job-x");
|
const origUrl0 = useFileStore.getState().entries[0].blobUrl;
|
||||||
useFileStore.getState().setProcessedUrl("blob:proc");
|
const origUrl1 = useFileStore.getState().entries[1].blobUrl;
|
||||||
useFileStore.getState().setProcessing(true);
|
|
||||||
useFileStore.getState().setError("oops");
|
|
||||||
useFileStore.getState().setSizes(100, 50);
|
|
||||||
revokeObjectURL.mockClear();
|
revokeObjectURL.mockClear();
|
||||||
|
|
||||||
useFileStore.getState().reset();
|
useFileStore.getState().reset();
|
||||||
|
|
||||||
expect(revokeObjectURL).toHaveBeenCalledWith("blob:to-revoke");
|
expect(revokeObjectURL).toHaveBeenCalledWith(origUrl0);
|
||||||
|
expect(revokeObjectURL).toHaveBeenCalledWith("blob:proc");
|
||||||
|
expect(revokeObjectURL).toHaveBeenCalledWith(origUrl1);
|
||||||
|
|
||||||
const s = useFileStore.getState();
|
const s = useFileStore.getState();
|
||||||
expect(s.files).toEqual([]);
|
expect(s.entries).toEqual([]);
|
||||||
expect(s.jobId).toBeNull();
|
expect(s.selectedIndex).toBe(0);
|
||||||
expect(s.processedUrl).toBeNull();
|
expect(s.batchZipBlob).toBeNull();
|
||||||
expect(s.originalBlobUrl).toBeNull();
|
expect(s.batchZipFilename).toBeNull();
|
||||||
expect(s.processing).toBe(false);
|
expect(s.processing).toBe(false);
|
||||||
expect(s.error).toBeNull();
|
expect(s.error).toBeNull();
|
||||||
expect(s.originalSize).toBeNull();
|
|
||||||
expect(s.processedSize).toBeNull();
|
|
||||||
expect(s.selectedFileName).toBeNull();
|
|
||||||
expect(s.selectedFileSize).toBeNull();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("reset when originalBlobUrl is already null does NOT call revokeObjectURL", () => {
|
it("reset with no entries does not call revokeObjectURL", () => {
|
||||||
// Start from a clean state (no files set)
|
|
||||||
revokeObjectURL.mockClear();
|
revokeObjectURL.mockClear();
|
||||||
useFileStore.getState().reset();
|
useFileStore.getState().reset();
|
||||||
expect(revokeObjectURL).not.toHaveBeenCalled();
|
expect(revokeObjectURL).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
// -- State transition sequences -------------------------------------------
|
// -- Backward compat getters ----------------------------------------------
|
||||||
|
|
||||||
it("setFiles -> setProcessing(true) -> setError -> processing is false", () => {
|
it("files getter maps entries to File[]", () => {
|
||||||
useFileStore.getState().setFiles([makeFile("t.png")]);
|
const f1 = makeFile("a.png");
|
||||||
useFileStore.getState().setProcessing(true);
|
const f2 = makeFile("b.png");
|
||||||
expect(useFileStore.getState().processing).toBe(true);
|
useFileStore.getState().setFiles([f1, f2]);
|
||||||
|
|
||||||
useFileStore.getState().setError("fail");
|
const s = useFileStore.getState();
|
||||||
expect(useFileStore.getState().processing).toBe(false);
|
expect(s.files).toEqual([f1, f2]);
|
||||||
expect(useFileStore.getState().error).toBe("fail");
|
expect(s.files[0]).toBe(f1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("setFiles -> setProcessing(true) -> setProcessedUrl -> setProcessing(false) (happy path)", () => {
|
it("currentEntry returns entry at selectedIndex", () => {
|
||||||
useFileStore.getState().setFiles([makeFile("t.png")]);
|
useFileStore.getState().setFiles([makeFile("a.png"), makeFile("b.png")]);
|
||||||
useFileStore.getState().setProcessing(true);
|
useFileStore.getState().setSelectedIndex(1);
|
||||||
expect(useFileStore.getState().processing).toBe(true);
|
|
||||||
|
|
||||||
useFileStore.getState().setProcessedUrl("blob:done");
|
expect(useFileStore.getState().currentEntry?.file.name).toBe("b.png");
|
||||||
// processedUrl does NOT auto-clear processing
|
});
|
||||||
expect(useFileStore.getState().processing).toBe(true);
|
|
||||||
|
|
||||||
useFileStore.getState().setProcessing(false);
|
it("currentEntry returns undefined when no entries", () => {
|
||||||
expect(useFileStore.getState().processing).toBe(false);
|
expect(useFileStore.getState().currentEntry).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("selectedFileName returns current entry file name", () => {
|
||||||
|
useFileStore.getState().setFiles([makeFile("photo.png")]);
|
||||||
|
expect(useFileStore.getState().selectedFileName).toBe("photo.png");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("selectedFileName returns null when no entries", () => {
|
||||||
|
expect(useFileStore.getState().selectedFileName).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("selectedFileSize returns current entry file size", () => {
|
||||||
|
useFileStore.getState().setFiles([makeFile("photo.png", 2048)]);
|
||||||
|
expect(useFileStore.getState().selectedFileSize).toBe(2048);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("selectedFileSize returns null when no entries", () => {
|
||||||
|
expect(useFileStore.getState().selectedFileSize).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("originalBlobUrl returns current entry blobUrl", () => {
|
||||||
|
useFileStore.getState().setFiles([makeFile("a.png")]);
|
||||||
|
expect(useFileStore.getState().originalBlobUrl).toBe(
|
||||||
|
useFileStore.getState().entries[0].blobUrl,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("originalBlobUrl returns null when no entries", () => {
|
||||||
|
expect(useFileStore.getState().originalBlobUrl).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("processedUrl returns current entry processedUrl", () => {
|
||||||
|
useFileStore.getState().setFiles([makeFile("a.png")]);
|
||||||
|
useFileStore.getState().updateEntry(0, { processedUrl: "blob:done" });
|
||||||
expect(useFileStore.getState().processedUrl).toBe("blob:done");
|
expect(useFileStore.getState().processedUrl).toBe("blob:done");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("rapid setFiles calls only keep the latest state and revoke each prior URL", () => {
|
it("originalSize returns current entry originalSize", () => {
|
||||||
createObjectURL
|
useFileStore.getState().setFiles([makeFile("a.png", 999)]);
|
||||||
.mockReturnValueOnce("blob:1")
|
expect(useFileStore.getState().originalSize).toBe(999);
|
||||||
.mockReturnValueOnce("blob:2")
|
|
||||||
.mockReturnValueOnce("blob:3");
|
|
||||||
|
|
||||||
useFileStore.getState().setFiles([makeFile("a.png")]);
|
|
||||||
useFileStore.getState().setFiles([makeFile("b.png")]);
|
|
||||||
useFileStore.getState().setFiles([makeFile("c.png")]);
|
|
||||||
|
|
||||||
expect(revokeObjectURL).toHaveBeenCalledWith("blob:1");
|
|
||||||
expect(revokeObjectURL).toHaveBeenCalledWith("blob:2");
|
|
||||||
expect(revokeObjectURL).toHaveBeenCalledTimes(2);
|
|
||||||
expect(useFileStore.getState().originalBlobUrl).toBe("blob:3");
|
|
||||||
expect(useFileStore.getState().selectedFileName).toBe("c.png");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("setError during processing, then undoProcessing, then retry cycle works", () => {
|
it("processedSize returns current entry processedSize", () => {
|
||||||
useFileStore.getState().setFiles([makeFile("retry.png")]);
|
useFileStore.getState().setFiles([makeFile("a.png")]);
|
||||||
useFileStore.getState().setProcessing(true);
|
useFileStore.getState().updateEntry(0, { processedSize: 500 });
|
||||||
useFileStore.getState().setError("timeout");
|
expect(useFileStore.getState().processedSize).toBe(500);
|
||||||
expect(useFileStore.getState().processing).toBe(false);
|
});
|
||||||
|
|
||||||
useFileStore.getState().undoProcessing();
|
it("hasFiles returns true when entries exist", () => {
|
||||||
expect(useFileStore.getState().error).toBeNull();
|
expect(useFileStore.getState().hasFiles).toBe(false);
|
||||||
expect(useFileStore.getState().files).toHaveLength(1);
|
useFileStore.getState().setFiles([makeFile("a.png")]);
|
||||||
|
expect(useFileStore.getState().hasFiles).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
// Retry
|
it("allProcessed returns true when all entries are completed", () => {
|
||||||
useFileStore.getState().setProcessing(true);
|
useFileStore.getState().setFiles([makeFile("a.png"), makeFile("b.png")]);
|
||||||
expect(useFileStore.getState().processing).toBe(true);
|
expect(useFileStore.getState().allProcessed).toBe(false);
|
||||||
useFileStore.getState().setProcessedUrl("blob:retry-ok");
|
|
||||||
useFileStore.getState().setProcessing(false);
|
useFileStore.getState().updateEntry(0, { status: "completed" });
|
||||||
expect(useFileStore.getState().processedUrl).toBe("blob:retry-ok");
|
expect(useFileStore.getState().allProcessed).toBe(false);
|
||||||
|
|
||||||
|
useFileStore.getState().updateEntry(1, { status: "completed" });
|
||||||
|
expect(useFileStore.getState().allProcessed).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("allProcessed returns false when no entries", () => {
|
||||||
|
expect(useFileStore.getState().allProcessed).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
// -- setProcessedUrl (backward compat, updates current entry) -------------
|
||||||
|
|
||||||
|
it("setProcessedUrl updates current entry processedUrl and status", () => {
|
||||||
|
useFileStore.getState().setFiles([makeFile("a.png")]);
|
||||||
|
useFileStore.getState().setProcessedUrl("blob:result");
|
||||||
|
|
||||||
|
const entry = useFileStore.getState().entries[0];
|
||||||
|
expect(entry.processedUrl).toBe("blob:result");
|
||||||
|
expect(entry.status).toBe("completed");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("setProcessedUrl with null resets current entry", () => {
|
||||||
|
useFileStore.getState().setFiles([makeFile("a.png")]);
|
||||||
|
useFileStore.getState().setProcessedUrl("blob:result");
|
||||||
|
useFileStore.getState().setProcessedUrl(null);
|
||||||
|
|
||||||
|
const entry = useFileStore.getState().entries[0];
|
||||||
|
expect(entry.processedUrl).toBeNull();
|
||||||
|
expect(entry.status).toBe("pending");
|
||||||
|
});
|
||||||
|
|
||||||
|
// -- setSizes (backward compat, updates current entry) --------------------
|
||||||
|
|
||||||
|
it("setSizes updates current entry sizes", () => {
|
||||||
|
useFileStore.getState().setFiles([makeFile("a.png", 1000)]);
|
||||||
|
useFileStore.getState().setSizes(1000, 500);
|
||||||
|
|
||||||
|
const entry = useFileStore.getState().entries[0];
|
||||||
|
expect(entry.originalSize).toBe(1000);
|
||||||
|
expect(entry.processedSize).toBe(500);
|
||||||
|
});
|
||||||
|
|
||||||
|
// -- setJobId (no-op for compat) ------------------------------------------
|
||||||
|
|
||||||
|
it("setJobId is a no-op (does not throw)", () => {
|
||||||
|
expect(() => useFileStore.getState().setJobId("job-abc")).not.toThrow();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user