import { fireEvent, render, screen, waitFor } from "@testing-library/react"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import * as mediaUploadEngine from "@/api/media-upload-engine"; import type { MediaUploadEndpointSet } from "@/api/media-upload-client"; import { MediaUploadNetworkError } from "@/api/media-upload-client"; import type { MediaChunkAckDto } from "@/api/media-upload-contracts"; import * as mediaLimits from "@/domain/work-orders/utils/emergency-media-limits"; import { vendorPortalMediaApi } from "@/domain/vendor-portal/api/vendor-portal-media-api"; import type { VendorMediaItem, VendorPortalMedia, } from "@/domain/vendor-portal/types/vendor-portal"; import { VendorMediaSection } from "@/app/v/[token]/dispatch/_components/vendor-media-section"; const MEDIA: VendorPortalMedia = { policy: { workOrderType: "Emergency", canUpload: true, limits: { photoMaxBytes: 10_000_000, videoMaxBytes: 100_000_000, videoMaxSeconds: 90, maxPhotos: 10, maxVideos: 3, }, remaining: { photos: 9, videos: 3 }, }, items: [], }; function renderVendorMedia() { const queryClient = new QueryClient({ defaultOptions: { mutations: { retry: false }, queries: { retry: false } }, }); return render( , ); } describe("Vendor media controls", () => { beforeEach(() => { vi.spyOn(vendorPortalMediaApi, "getMedia").mockResolvedValue(MEDIA); vi.spyOn(vendorPortalMediaApi, "endpoints").mockReturnValue({} as never); vi.spyOn(mediaLimits, "validateMediaFileAgainstLimits").mockResolvedValue({ ok: true }); }); afterEach(() => { vi.restoreAllMocks(); }); it("retries the same file with the same idempotency key after a partial failure", async () => { const upload = vi .spyOn(mediaUploadEngine, "uploadFileResumable") .mockRejectedValueOnce(new MediaUploadNetworkError()) .mockResolvedValue({ uploadId: "upload-1", state: "Completed", chunkSizeBytes: 5_242_880, sizeBytes: 5, receivedChunks: [0], expiresAt: "2026-09-24T00:00:00Z", attachment: { id: 1 }, }); renderVendorMedia(); const file = new File(["video"], "clip.mov", { type: "video/quicktime" }); await waitFor(() => expect(screen.getByRole("button", { name: "Upload photo or video" })).toBeEnabled(), ); fireEvent.change(screen.getByLabelText("Choose photos or videos"), { target: { files: [file] }, }); await waitFor(() => expect(screen.getByRole("button", { name: "Retry upload" })).toBeInTheDocument(), ); fireEvent.click(screen.getByRole("button", { name: "Retry upload" })); await waitFor(() => expect(upload).toHaveBeenCalledTimes(2)); expect(upload.mock.calls[0]?.[0].file).toBe(file); expect(upload.mock.calls[1]?.[0].file).toBe(file); expect(upload.mock.calls[1]?.[0].idempotencyKey).toBe(upload.mock.calls[0]?.[0].idempotencyKey); }); it("offers same-session retry after a transient HTTP 503", async () => { const unavailable = Object.assign(new Error("Upload request failed (HTTP 503)."), { name: "MediaUploadHttpError", status: 503, }); const upload = vi .spyOn(mediaUploadEngine, "uploadFileResumable") .mockRejectedValueOnce(unavailable) .mockResolvedValue({ uploadId: "upload-503", state: "Completed", chunkSizeBytes: 5_242_880, sizeBytes: 5, receivedChunks: [0], expiresAt: "2026-09-24T00:00:00Z", attachment: { id: 2 }, }); renderVendorMedia(); const file = new File(["video"], "clip.mov", { type: "video/quicktime" }); await waitFor(() => expect(screen.getByRole("button", { name: "Upload photo or video" })).toBeEnabled(), ); fireEvent.change(screen.getByLabelText("Choose photos or videos"), { target: { files: [file] }, }); await waitFor(() => expect(screen.getByRole("button", { name: "Retry upload" })).toBeInTheDocument(), ); fireEvent.click(screen.getByRole("button", { name: "Retry upload" })); await waitFor(() => expect(upload).toHaveBeenCalledTimes(2)); expect(upload.mock.calls[1]?.[0].file).toBe(file); expect(upload.mock.calls[1]?.[0].idempotencyKey).toBe(upload.mock.calls[0]?.[0].idempotencyKey); }); it("aborts the active vendor upload from the visible Cancel control", async () => { const cancel = vi.fn().mockResolvedValue(undefined); const endpoints: MediaUploadEndpointSet = { create: vi.fn().mockResolvedValue({ uploadId: "upload-cancel", state: "Created", chunkSizeBytes: 5_242_880, sizeBytes: 5, receivedChunks: [], expiresAt: "2026-09-24T00:00:00Z", }), putChunk: vi.fn( ({ signal }: Parameters[0]) => new Promise((_resolve, reject) => { const abort = () => reject(new DOMException("Aborted", "AbortError")); if (signal?.aborted) abort(); else signal?.addEventListener("abort", abort, { once: true }); }), ), commit: vi.fn(), getStatus: vi.fn(), cancel, }; vi.mocked(vendorPortalMediaApi.endpoints).mockReturnValue(endpoints); renderVendorMedia(); const file = new File(["video"], "clip.mov", { type: "video/quicktime" }); await waitFor(() => expect(screen.getByRole("button", { name: "Upload photo or video" })).toBeEnabled(), ); fireEvent.change(screen.getByLabelText("Choose photos or videos"), { target: { files: [file] }, }); await waitFor(() => expect(screen.getByRole("button", { name: "Cancel upload" })).toBeInTheDocument(), ); fireEvent.click(screen.getByRole("button", { name: "Cancel upload" })); await waitFor(() => expect(screen.getByText("Upload canceled.")).toBeInTheDocument()); expect(endpoints.putChunk).toHaveBeenCalledWith( expect.objectContaining({ signal: expect.objectContaining({ aborted: true }), }), ); await waitFor(() => expect(cancel).toHaveBeenCalledWith("upload-cancel")); }); it("opens media from the View click before fetching and revokes its object URL on close", async () => { const item: VendorMediaItem = { id: 14, originalFileName: "photo.jpg", contentType: "image/jpeg", sizeBytes: 5, kind: "Photo", category: "Extra", scanStatus: "Passed", createdAt: "2026-09-23T00:00:00Z", canView: true, }; vi.spyOn(vendorPortalMediaApi, "getMedia").mockResolvedValue({ ...MEDIA, items: [item], }); const addEventListener = vi.fn(); const previewWindow = { opener: window, closed: false, document: { title: "", body: { textContent: "" } }, location: { replace: vi.fn() }, addEventListener, close: vi.fn(), } as unknown as Window; const open = vi.spyOn(window, "open").mockReturnValue(previewWindow); const createObjectURL = vi.spyOn(URL, "createObjectURL").mockReturnValue("blob:vendor-media"); const revokeObjectURL = vi.spyOn(URL, "revokeObjectURL").mockImplementation(() => undefined); const fetchContent = vi .spyOn(vendorPortalMediaApi, "fetchContent") .mockImplementation(async (_token, _dispatchId, mediaId) => { expect(open).toHaveBeenCalledWith("about:blank", "_blank"); expect(mediaId).toBe(item.id); return new Blob(["image"], { type: item.contentType }); }); renderVendorMedia(); fireEvent.click(await screen.findByRole("button", { name: "View" })); await waitFor(() => expect(fetchContent).toHaveBeenCalledWith("vendor-token", 9, item.id)); expect(createObjectURL).toHaveBeenCalledOnce(); expect(previewWindow.document.title).toBe(item.originalFileName); expect(previewWindow.location.replace).toHaveBeenCalledWith("blob:vendor-media"); const pageHide = addEventListener.mock.calls[0]?.[1] as EventListener | undefined; pageHide?.(new Event("pagehide")); expect(revokeObjectURL).toHaveBeenCalledWith("blob:vendor-media"); }); });