import { suppressExpectedErrorMessages } from "@tests/vitest/helpers"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import type { NewUploadItem } from "@/composables/upload/uploadItemTypes"; import type { CollectionConfig } from "@/composables/uploadQueue"; import { useUploadState } from "./uploadState"; // useUserLocalStorage is auto-mocked globally (returns a plain ref) — see tests/vitest/setup.ts // The module-level singleton refs persist between tests, so we call clearAll() in beforeEach. function makePastedItem(name = "file.txt", content = "hello world"): NewUploadItem { return { uploadMode: "paste-content", name, content, size: content.length, targetHistoryId: "hist_1", dbkey: "?", extension: "auto", spaceToTab: false, toPosixLines: false, deferred: false, }; } const BATCH_CONFIG: CollectionConfig = { name: "My Collection", type: "list", hideSourceItems: false, historyId: "hist_1", }; describe("useUploadState", () => { let state: ReturnType; beforeEach(() => { state = useUploadState(); state.clearAll(); }); afterEach(() => { vi.restoreAllMocks(); }); describe("initial state", () => { it("has no uploads and all counters at zero", () => { expect(state.activeItems.value).toHaveLength(0); expect(state.activeBatches.value).toHaveLength(0); expect(state.hasUploads.value).toBe(false); expect(state.isUploading.value).toBe(false); expect(state.hasCompleted.value).toBe(false); expect(state.uploadingCount.value).toBe(0); expect(state.completedCount.value).toBe(0); expect(state.errorCount.value).toBe(0); expect(state.totalProgress.value).toBe(0); expect(state.orderedUploadItems.value).toHaveLength(0); }); }); describe("addUploadItem", () => { it("returns a unique ID and adds item to activeItems", () => { const id = state.addUploadItem(makePastedItem()); expect(id).toBeTruthy(); expect(state.activeItems.value).toHaveLength(1); expect(state.hasUploads.value).toBe(true); }); it("initializes item with queued status, zero progress, and correct name", () => { const id = state.addUploadItem(makePastedItem("report.txt", "content")); const item = state.activeItems.value.find((i) => i.id === id); expect(item?.status).toBe("queued"); expect(item?.progress).toBe(0); expect(item?.name).toBe("report.txt"); }); it("standalone item appears in standaloneUploads and orderedUploadItems", () => { const id = state.addUploadItem(makePastedItem()); expect(state.standaloneUploads.value.map((i) => i.id)).toContain(id); expect(state.orderedUploadItems.value).toHaveLength(1); expect(state.orderedUploadItems.value[0]?.type).toBe("upload"); }); it("item associated with a batchId does not appear in standaloneUploads", () => { const batchId = state.addBatch(BATCH_CONFIG, []); const id = state.addUploadItem(makePastedItem(), batchId); const item = state.activeItems.value.find((i) => i.id === id); expect(item?.batchId).toBe(batchId); expect(state.standaloneUploads.value.map((i) => i.id)).not.toContain(id); }); }); describe("addBatch", () => { it("creates a batch with uploading status, the provided upload IDs, and no collectionId", () => { const id1 = state.addUploadItem(makePastedItem("a.txt")); const id2 = state.addUploadItem(makePastedItem("b.txt")); const batchId = state.addBatch(BATCH_CONFIG, [id1, id2]); const batch = state.getBatch(batchId); expect(batch?.status).toBe("uploading"); expect(batch?.uploadIds).toEqual([id1, id2]); expect(batch?.datasetIds).toEqual([]); expect(batch?.collectionId).toBeUndefined(); }); it("batch appears in batchesWithProgress with aggregated upload data", () => { const id = state.addUploadItem(makePastedItem()); const batchId = state.addBatch(BATCH_CONFIG, [id]); const bwp = state.batchesWithProgress.value.find((b) => b.id === batchId); expect(bwp?.uploads).toHaveLength(1); expect(bwp?.progress).toBe(0); expect(bwp?.allCompleted).toBe(false); expect(bwp?.hasError).toBe(false); }); }); describe("computed counts", () => { it("tallies uploading, completed, and errored items independently", () => { const uploadingId = state.addUploadItem(makePastedItem("uploading.txt")); const completedId = state.addUploadItem(makePastedItem("done.txt")); const erroredId = state.addUploadItem(makePastedItem("failed.txt")); state.setStatus(uploadingId, "uploading"); state.updateProgress(completedId, 100); // auto-marks as completed state.setError(erroredId, "network error"); expect(state.uploadingCount.value).toBe(1); expect(state.completedCount.value).toBe(1); expect(state.errorCount.value).toBe(1); expect(state.isUploading.value).toBe(true); expect(state.hasCompleted.value).toBe(true); }); it("isUploading is false when no items are in uploading or processing state", () => { const id = state.addUploadItem(makePastedItem()); state.updateProgress(id, 100); // completed expect(state.isUploading.value).toBe(false); }); }); describe("progress tracking", () => { it("updateProgress updates the item's progress value", () => { const id = state.addUploadItem(makePastedItem()); state.setStatus(id, "uploading"); state.updateProgress(id, 50); const item = state.activeItems.value.find((i) => i.id === id)!; expect(item.progress).toBe(50); }); it("reaching 100% auto-transitions item status to completed", () => { const id = state.addUploadItem(makePastedItem()); state.setStatus(id, "uploading"); state.updateProgress(id, 100); const item = state.activeItems.value.find((i) => i.id === id)!; expect(item.status).toBe("completed"); }); it("totalProgress is the average progress across all items", () => { const id1 = state.addUploadItem(makePastedItem("a.txt")); const id2 = state.addUploadItem(makePastedItem("b.txt")); state.updateProgress(id1, 40); state.updateProgress(id2, 60); expect(state.totalProgress.value).toBe(50); }); it("totalSizeBytes sums item sizes and uploadedSizeBytes reflects partial progress", () => { // makePastedItem uses content.length as size: "hello" = 5, "world!" = 6 const id1 = state.addUploadItem(makePastedItem("a.txt", "hello")); const id2 = state.addUploadItem(makePastedItem("b.txt", "world!")); state.updateProgress(id1, 100); state.updateProgress(id2, 0); expect(state.totalSizeBytes.value).toBe(11); expect(state.uploadedSizeBytes.value).toBe(5); // only id1 is fully uploaded }); }); describe("batch lifecycle", () => { function setupBatch() { const id1 = state.addUploadItem(makePastedItem("a.txt")); const id2 = state.addUploadItem(makePastedItem("b.txt")); const batchId = state.addBatch(BATCH_CONFIG, [id1, id2]); return { id1, id2, batchId }; } it("updateBatchStatus transitions the batch through status stages", () => { const { batchId } = setupBatch(); state.updateBatchStatus(batchId, "creating-collection"); expect(state.getBatch(batchId)?.status).toBe("creating-collection"); state.updateBatchStatus(batchId, "completed"); expect(state.getBatch(batchId)?.status).toBe("completed"); }); it("setBatchCollectionId stores the created collection ID on the batch", () => { const { batchId } = setupBatch(); state.setBatchCollectionId(batchId, "col_abc"); expect(state.getBatch(batchId)?.collectionId).toBe("col_abc"); }); it("addBatchDatasetId accumulates dataset IDs in order", () => { const { batchId } = setupBatch(); state.addBatchDatasetId(batchId, "ds_1"); state.addBatchDatasetId(batchId, "ds_2"); expect(state.getBatch(batchId)?.datasetIds).toEqual(["ds_1", "ds_2"]); }); it("batchesWithProgress.allCompleted is true when all uploads reach 100%", () => { const { id1, id2, batchId } = setupBatch(); state.setStatus(id1, "uploading"); state.setStatus(id2, "uploading"); state.updateProgress(id1, 100); state.updateProgress(id2, 100); const bwp = state.batchesWithProgress.value.find((b) => b.id === batchId)!; expect(bwp.allCompleted).toBe(true); expect(bwp.progress).toBe(100); }); it("batchesWithProgress.hasError is true when any upload fails", () => { const { id1, batchId } = setupBatch(); state.setError(id1, "upload error"); const bwp = state.batchesWithProgress.value.find((b) => b.id === batchId)!; expect(bwp.hasError).toBe(true); }); }); describe("error handling", () => { it("setError marks the item with error status and stores the message", () => { const id = state.addUploadItem(makePastedItem()); state.setError(id, "network failure"); const item = state.activeItems.value.find((i) => i.id === id)!; expect(item.status).toBe("error"); expect(item.error).toBe("network failure"); }); it("setBatchError marks the batch with error status and stores the message", () => { const expectedMessage = "collection creation failed"; suppressExpectedErrorMessages([expectedMessage]); const batchId = state.addBatch(BATCH_CONFIG, []); state.setBatchError(batchId, expectedMessage); const batch = state.getBatch(batchId)!; expect(batch.status).toBe("error"); expect(batch.error).toBe(expectedMessage); }); }); describe("clearCompleted", () => { it("removes completed items while preserving uploading and errored items", () => { const uploadingId = state.addUploadItem(makePastedItem("active.txt")); const completedId = state.addUploadItem(makePastedItem("done.txt")); const erroredId = state.addUploadItem(makePastedItem("failed.txt")); state.setStatus(uploadingId, "uploading"); state.updateProgress(completedId, 100); state.setError(erroredId, "oops"); state.clearCompleted(); const remainingIds = state.activeItems.value.map((i) => i.id); expect(remainingIds).toContain(uploadingId); expect(remainingIds).toContain(erroredId); expect(remainingIds).not.toContain(completedId); }); it("removes a completed batch after all its items are cleared", () => { const id1 = state.addUploadItem(makePastedItem("a.txt")); const id2 = state.addUploadItem(makePastedItem("b.txt")); const batchId = state.addBatch(BATCH_CONFIG, [id1, id2]); state.updateBatchStatus(batchId, "completed"); state.updateProgress(id1, 100); state.updateProgress(id2, 100); state.clearCompleted(); expect(state.activeBatches.value.find((b) => b.id === batchId)).toBeUndefined(); }); it("keeps a batch with at least one non-completed item after clearing", () => { const completedId = state.addUploadItem(makePastedItem("done.txt")); const uploadingId = state.addUploadItem(makePastedItem("active.txt")); const batchId = state.addBatch(BATCH_CONFIG, [completedId, uploadingId]); state.updateProgress(completedId, 100); state.setStatus(uploadingId, "uploading"); state.clearCompleted(); // Batch stays because uploadingId is still active expect(state.activeBatches.value.find((b) => b.id === batchId)).toBeDefined(); // But the completed item is gone expect(state.activeItems.value.find((i) => i.id === completedId)).toBeUndefined(); }); }); describe("clearAll", () => { it("empties all items, batches, and resets computed flags", () => { state.addUploadItem(makePastedItem()); state.addBatch(BATCH_CONFIG, []); state.clearAll(); expect(state.activeItems.value).toHaveLength(0); expect(state.activeBatches.value).toHaveLength(0); expect(state.hasUploads.value).toBe(false); }); }); });