import { act, renderHook, waitFor } from "@testing-library/react"; import { describe, expect, it, vi } from "vitest"; import { useMediaUploadSession } from "@/app/(protected)/workorders/_hooks/use-media-upload-session"; function makeFile(name = "photo.jpg") { return new File(["bytes"], name, { type: "image/jpeg" }); } function boardDetail(id: number) { return { info: { id }, media: [], comments: [], audit: [], completion: { docStatus: "", template: null, signOffName: "", signOffAttachment: "", signOffSignature: "", dispatchSignoffs: [], }, } as unknown as Parameters[0]; } describe("useMediaUploadSession retry / cancel", () => { it("retry upserts the same localId instead of appending a duplicate", async () => { let rejectFirst!: (error: Error) => void; const uploadMutateAsync = vi .fn() .mockImplementationOnce( () => new Promise((_resolve, reject) => { rejectFirst = reject; }), ) .mockImplementation(() => new Promise(() => undefined)); const { result } = renderHook(() => useMediaUploadSession(undefined, uploadMutateAsync)); const localId = "local-1"; const file = makeFile(); act(() => { result.current.startUploads([ { localId, file, onProgress: () => undefined, }, ]); }); await waitFor(() => { expect(result.current.uploads).toHaveLength(1); }); const firstSignal = uploadMutateAsync.mock.calls[0]?.[0]?.signal as AbortSignal; act(() => { rejectFirst(new Error("network")); }); await waitFor(() => { expect(result.current.uploads[0]?.uploadStatus).toBe("failed"); }); act(() => { result.current.retryUpload(localId); }); await waitFor(() => { expect(result.current.uploads).toHaveLength(1); expect(result.current.uploads[0]?.id).toBe(localId); expect(result.current.uploads[0]?.uploadStatus).toBe("uploading"); }); expect(uploadMutateAsync).toHaveBeenCalledTimes(2); const retrySignal = uploadMutateAsync.mock.calls[1]?.[0]?.signal as AbortSignal; expect(retrySignal).toBeInstanceOf(AbortSignal); expect(retrySignal).not.toBe(firstSignal); expect(retrySignal.aborted).toBe(false); }); it("fail → retry → cancel aborts the replacement upload and records no success", async () => { let rejectFirst!: (error: Error) => void; let resolveRetry!: (value: unknown) => void; const uploadMutateAsync = vi .fn() .mockImplementationOnce( () => new Promise((_resolve, reject) => { rejectFirst = reject; }), ) .mockImplementation( ({ signal }: { signal?: AbortSignal }) => new Promise((resolve, reject) => { resolveRetry = resolve; signal?.addEventListener("abort", () => { const error = new Error("aborted"); error.name = "AbortError"; reject(error); }); }), ); const { result } = renderHook(() => useMediaUploadSession(undefined, uploadMutateAsync)); const localId = "local-retry"; const file = makeFile("retry.jpg"); act(() => { result.current.startUploads([ { localId, file, onProgress: () => undefined, }, ]); }); act(() => { rejectFirst(new Error("upload failed")); }); await waitFor(() => { expect(result.current.uploads[0]?.uploadStatus).toBe("failed"); }); act(() => { result.current.retryUpload(localId); }); await waitFor(() => { expect(result.current.uploads[0]?.uploadStatus).toBe("uploading"); }); const retrySignal = uploadMutateAsync.mock.calls[1]?.[0]?.signal as AbortSignal; act(() => { result.current.cancelUpload(localId); }); await waitFor(() => { expect(result.current.uploads).toHaveLength(0); }); expect(retrySignal.aborted).toBe(true); // Cancelling must not leave a successful settle path. expect(() => resolveRetry({})).not.toThrow(); await waitFor(() => { expect(result.current.uploads).toHaveLength(0); }); }); it("clears local uploads and aborts in-flight work when the bound WO changes", async () => { const uploadMutateAsync = vi.fn().mockImplementation( ({ signal }: { signal?: AbortSignal }) => new Promise((_resolve, reject) => { signal?.addEventListener("abort", () => { const error = new Error("aborted"); error.name = "AbortError"; reject(error); }); }), ); const { result, rerender } = renderHook( ({ detail }) => useMediaUploadSession(detail, uploadMutateAsync), { initialProps: { detail: boardDetail(1) } }, ); act(() => { result.current.startUploads([ { localId: "local-wo-a", file: makeFile("a.jpg"), onProgress: () => undefined, }, ]); }); await waitFor(() => { expect(result.current.uploads).toHaveLength(1); }); const firstSignal = uploadMutateAsync.mock.calls[0]?.[0]?.signal as AbortSignal; rerender({ detail: boardDetail(2) }); await waitFor(() => { expect(result.current.uploads).toHaveLength(0); }); expect(firstSignal.aborted).toBe(true); }); });