diff --git a/src/api/media-upload-contracts.ts b/src/api/media-upload-contracts.ts index baf1663a..7a4644d1 100644 --- a/src/api/media-upload-contracts.ts +++ b/src/api/media-upload-contracts.ts @@ -88,6 +88,9 @@ export function safeMediaUploadErrorMessage(error: Error): string { if (error instanceof MediaUploadScanTimeoutError) { return "The upload is still processing. Check back shortly to see the result."; } + if (error instanceof MediaUploadCanceledError) { + return "The upload session was canceled."; + } if (error.name === "AbortError") { return "Upload canceled."; } diff --git a/src/test/api/media-upload-engine.test.ts b/src/test/api/media-upload-engine.test.ts index e06b9264..3f4bbc0a 100644 --- a/src/test/api/media-upload-engine.test.ts +++ b/src/test/api/media-upload-engine.test.ts @@ -9,6 +9,10 @@ import { uploadFileResumable, } from "@/api/media-upload-engine"; import { computeSha256Hex, createMediaUploadEndpoints } from "@/api/media-upload-client"; +import { + MediaUploadCanceledError, + safeMediaUploadErrorMessage, +} from "@/api/media-upload-contracts"; type CapturedRequest = { method: string; url: string; headers: Headers; body: unknown }; @@ -500,6 +504,40 @@ describe("resumable media upload engine", () => { }); }); +describe("safeMediaUploadErrorMessage", () => { + it("maps a server-canceled session to its own copy, distinct from a user abort", async () => { + const file = fileOf(8); + handler = async (request) => { + if (request.method === "POST" && request.url === BASE) { + return jsonResponse(createdSession(8, 8)); + } + if (request.method === "PUT" && /chunks\/0$/.test(request.url)) { + return jsonResponse({ index: 0, sha256: "x", sizeBytes: 8, receivedChunks: [0] }); + } + if (request.method === "POST" && request.url.endsWith("/commit")) { + return jsonResponse({ ...createdSession(8, 8), state: "Scanning" }, 202); + } + if (request.method === "GET" && request.url === `${BASE}/u1`) { + return jsonResponse({ ...createdSession(8, 8), state: "Canceled" }); + } + return jsonResponse({}); + }; + + const error = await uploadFileResumable({ + file, + endpoints: endpointsWith(), + surface: "WorkOrderTable", + pollDelayMs: 0, + }).catch((caught: unknown) => caught); + + expect(error).toBeInstanceOf(MediaUploadCanceledError); + expect(safeMediaUploadErrorMessage(error as Error)).toBe("The upload session was canceled."); + expect(safeMediaUploadErrorMessage(new DOMException("aborted", "AbortError"))).toBe( + "Upload canceled.", + ); + }); +}); + describe("chunk math helpers", () => { it("computes chunk counts and missing indices", () => { expect(chunkCountFor(0, 5_242_880)).toBe(1);