From 346760b9735f88cfc48be59a5385a15ba8aacc01 Mon Sep 17 00:00:00 2001 From: "arthur.bassi" Date: Tue, 4 Aug 2026 16:32:40 -0300 Subject: [PATCH] fix(work-orders): harden CompDoc generate order and media abort ownership Session owns AbortControllers for retry/cancel; refresh board state on Generate prerequisite failures; remove dead EditWorkorder completedDate mapper. --- .../comp-doc/use-comp-doc-dialog.tsx | 54 ++++-------- .../comp-doc/use-comp-doc-pdf-actions.ts | 3 + .../comp-doc/use-comp-doc-persist-flow.ts | 84 +++++++++++++++++++ .../detail/use-slide-over-media-state.ts | 1 - .../_components/media/media-upload-modal.tsx | 8 +- .../_components/media/media-uploader.tsx | 40 ++------- .../_hooks/use-media-upload-session.ts | 55 ++++++++++-- .../work-orders/mappers/work-order-mapper.ts | 11 --- ...mp-doc-dialog-completion-contract.test.tsx | 24 ++++++ .../use-media-upload-session.test.tsx | 36 ++++---- .../mappers/work-order-form-payload.test.ts | 22 ----- 11 files changed, 209 insertions(+), 129 deletions(-) create mode 100644 src/app/(protected)/workorders/_components/comp-doc/use-comp-doc-persist-flow.ts diff --git a/src/app/(protected)/workorders/_components/comp-doc/use-comp-doc-dialog.tsx b/src/app/(protected)/workorders/_components/comp-doc/use-comp-doc-dialog.tsx index 62179b51..69b71031 100644 --- a/src/app/(protected)/workorders/_components/comp-doc/use-comp-doc-dialog.tsx +++ b/src/app/(protected)/workorders/_components/comp-doc/use-comp-doc-dialog.tsx @@ -11,13 +11,10 @@ import type { WorkOrderTableRow } from "@/domain/work-orders/types/work-order-ta import { useWorkOrderBoardDetail } from "@/domain/work-orders/use-cases/use-work-order-detail"; import { useUploadCompletionDoc } from "@/domain/work-orders/use-cases/use-work-order-mutations"; import { resolveCompDocSite } from "@/domain/work-orders/utils/completion-doc-site"; -import { - DOC_STATE_CFG, - docStateOf, - fmtDocNow, -} from "@/domain/work-orders/utils/completion-doc-state"; +import { DOC_STATE_CFG, docStateOf } from "@/domain/work-orders/utils/completion-doc-state"; import { isWorkOrderDocumentsReadOnly } from "@/domain/work-orders/utils/work-order-media-limits"; import { useCompDocPdfActions } from "./use-comp-doc-pdf-actions"; +import { useCompDocPersistFlow } from "./use-comp-doc-persist-flow"; type UseCompDocDialogArgs = { row: WorkOrderTableRow; @@ -114,15 +111,21 @@ export function useCompDocDialog({ seaRep, }; - const persist = async (extra: Partial, status: CompDocSaveStatus) => { - await onSave({ notes, generatedAt, ...extra }, status); - }; - - const syncDateIfChanged = async () => { - if (editDateCompleted !== (info.completedDate || "")) { - await onDateCompletedChange?.(editDateCompleted); - } - }; + const { persist, syncDateIfChanged, refreshAuthoritativeState, handleSaveDraft, handleGenerate } = + useCompDocPersistFlow({ + workOrderId: row.id, + notes, + generatedAt, + editDateCompleted, + currentCompletedDate: info.completedDate || "", + onSave, + onDateCompletedChange, + onGenerated: (ts) => { + setGeneratedAt(ts); + setDocState("generated"); + }, + onDraftSaved: notifyDraftSaved, + }); function handleMarkNotRequired() { setDocState("not-required"); @@ -134,28 +137,6 @@ export function useCompDocDialog({ void persist({}, "No"); } - async function handleSaveDraft() { - try { - await syncDateIfChanged(); - await persist({}, "No"); - notifyDraftSaved(); - } catch (error) { - toast.error(error instanceof Error ? error.message : "Failed to save draft"); - } - } - - async function handleGenerate() { - const ts = fmtDocNow(); - try { - await syncDateIfChanged(); - await persist({ generatedAt: ts }, "Yes"); - setGeneratedAt(ts); - setDocState("generated"); - } catch (error) { - toast.error(error instanceof Error ? error.message : "Failed to generate document"); - } - } - const { handleDownloadPdf, handleUploadPdf } = useCompDocPdfActions({ info, signOffAttachment, @@ -168,6 +149,7 @@ export function useCompDocDialog({ syncDateIfChanged, setGeneratedAt, markGenerated: () => setDocState("generated"), + onPersistFailure: refreshAuthoritativeState, }); return { diff --git a/src/app/(protected)/workorders/_components/comp-doc/use-comp-doc-pdf-actions.ts b/src/app/(protected)/workorders/_components/comp-doc/use-comp-doc-pdf-actions.ts index f659d23f..6e9feb91 100644 --- a/src/app/(protected)/workorders/_components/comp-doc/use-comp-doc-pdf-actions.ts +++ b/src/app/(protected)/workorders/_components/comp-doc/use-comp-doc-pdf-actions.ts @@ -25,6 +25,7 @@ type UseCompDocPdfActionsArgs = { syncDateIfChanged: () => Promise; setGeneratedAt: (value: string) => void; markGenerated: () => void; + onPersistFailure?: () => void; }; export function useCompDocPdfActions({ @@ -39,6 +40,7 @@ export function useCompDocPdfActions({ syncDateIfChanged, setGeneratedAt, markGenerated, + onPersistFailure, }: UseCompDocPdfActionsArgs) { function handleDownloadPdf() { if (signOffAttachment) { @@ -75,6 +77,7 @@ export function useCompDocPdfActions({ setGeneratedAt(ts); markGenerated(); } catch (error) { + onPersistFailure?.(); toast.error( error instanceof Error ? error.message : "Failed to finalize completion document", ); diff --git a/src/app/(protected)/workorders/_components/comp-doc/use-comp-doc-persist-flow.ts b/src/app/(protected)/workorders/_components/comp-doc/use-comp-doc-persist-flow.ts new file mode 100644 index 00000000..57a89c17 --- /dev/null +++ b/src/app/(protected)/workorders/_components/comp-doc/use-comp-doc-persist-flow.ts @@ -0,0 +1,84 @@ +import { useQueryClient } from "@tanstack/react-query"; +import { toast } from "react-toastify"; +import type { CompDocData, CompDocSaveStatus } from "@/domain/work-orders/types/completion-doc"; +import { fmtDocNow } from "@/domain/work-orders/utils/completion-doc-state"; +import { queryKeys } from "@/infra/query-key/query-key"; + +type UseCompDocPersistFlowArgs = { + workOrderId: string | number; + notes: string; + generatedAt: string; + editDateCompleted: string; + currentCompletedDate: string; + onSave: (data: CompDocData, status: CompDocSaveStatus) => void | Promise; + onDateCompletedChange?: (date: string) => void | Promise; + onGenerated: (generatedAt: string) => void; + onDraftSaved: () => void; +}; + +export function useCompDocPersistFlow({ + workOrderId, + notes, + generatedAt, + editDateCompleted, + currentCompletedDate, + onSave, + onDateCompletedChange, + onGenerated, + onDraftSaved, +}: UseCompDocPersistFlowArgs) { + const queryClient = useQueryClient(); + + const refreshAuthoritativeState = () => { + void queryClient.invalidateQueries({ queryKey: queryKeys.workOrders.all }); + void queryClient.invalidateQueries({ + queryKey: queryKeys.workOrders.boardDetail(workOrderId), + }); + void queryClient.invalidateQueries({ + queryKey: queryKeys.workOrders.detail(workOrderId), + }); + }; + + const persist = async (extra: Partial, status: CompDocSaveStatus) => { + await onSave({ notes, generatedAt, ...extra }, status); + }; + + const syncDateIfChanged = async () => { + if (editDateCompleted !== currentCompletedDate) { + await onDateCompletedChange?.(editDateCompleted); + } + }; + + async function handleSaveDraft() { + try { + // Prerequisites first; docStatus last. + await syncDateIfChanged(); + await persist({}, "No"); + onDraftSaved(); + } catch (error) { + refreshAuthoritativeState(); + toast.error(error instanceof Error ? error.message : "Failed to save draft"); + } + } + + async function handleGenerate() { + const ts = fmtDocNow(); + try { + // Prerequisites first; only mark Generated after completedDate persists. + await syncDateIfChanged(); + await persist({ generatedAt: ts }, "Yes"); + onGenerated(ts); + } catch (error) { + refreshAuthoritativeState(); + toast.error(error instanceof Error ? error.message : "Failed to generate document"); + } + } + + return { + persist, + syncDateIfChanged, + refreshAuthoritativeState, + handleSaveDraft, + handleGenerate, + }; +} diff --git a/src/app/(protected)/workorders/_components/detail/use-slide-over-media-state.ts b/src/app/(protected)/workorders/_components/detail/use-slide-over-media-state.ts index cd221e08..0220f747 100644 --- a/src/app/(protected)/workorders/_components/detail/use-slide-over-media-state.ts +++ b/src/app/(protected)/workorders/_components/detail/use-slide-over-media-state.ts @@ -56,7 +56,6 @@ export function useSlideOverMediaState({ for (const request of requests) { await uploadMediaMutation.mutateAsync({ file: request.file, - signal: request.signal, onProgress: request.onProgress, }); } diff --git a/src/app/(protected)/workorders/_components/media/media-upload-modal.tsx b/src/app/(protected)/workorders/_components/media/media-upload-modal.tsx index 4b6c10c1..ee60f414 100644 --- a/src/app/(protected)/workorders/_components/media/media-upload-modal.tsx +++ b/src/app/(protected)/workorders/_components/media/media-upload-modal.tsx @@ -29,10 +29,8 @@ export function MediaUploadModal({ row, open, onOpenChange }: MediaUploadModalPr const categorizeMutation = useUpdateWorkOrderMediaCategory(row.id); const deleteMutation = useDeleteWorkOrderMedia(row.id); const isReadOnly = isWorkOrderDocumentsReadOnly(detail?.info.status ?? row.status); - const { uploads, startUploads, retryUpload, removeLocal, isLocal } = useMediaUploadSession( - detail, - (input) => uploadMutation.mutateAsync(input), - ); + const { uploads, startUploads, retryUpload, cancelUpload, removeLocal, isLocal } = + useMediaUploadSession(detail, (input) => uploadMutation.mutateAsync(input)); const pending = uploadMutation.isPending || deleteMutation.isPending || categorizeMutation.isPending; @@ -85,7 +83,7 @@ export function MediaUploadModal({ row, open, onOpenChange }: MediaUploadModalPr }} onCategorize={(id, category) => categorizeMutation.mutate({ mediaId: id, category })} onRetry={retryUpload} - onCancel={removeLocal} + onCancel={cancelUpload} readOnly={isReadOnly} pending={pending} /> diff --git a/src/app/(protected)/workorders/_components/media/media-uploader.tsx b/src/app/(protected)/workorders/_components/media/media-uploader.tsx index 4546c9da..69f4c364 100644 --- a/src/app/(protected)/workorders/_components/media/media-uploader.tsx +++ b/src/app/(protected)/workorders/_components/media/media-uploader.tsx @@ -1,4 +1,4 @@ -import { useRef, useState } from "react"; +import { useRef } from "react"; import { toast } from "react-toastify"; import type { UploadableMediaCategory } from "@/domain/work-orders/mappers/work-order-board-detail-mapper"; import type { MediaFile } from "@/domain/work-orders/types/work-order-media"; @@ -15,7 +15,6 @@ import { MediaUploaderGrid } from "./media-uploader-grid"; export type MediaUploadRequest = { localId: string; file: File; - signal: AbortSignal; onProgress: (percent: number) => void; }; @@ -25,7 +24,7 @@ type MediaUploaderProps = { onUpload?: (requests: MediaUploadRequest[]) => void; onRemove?: (id: string) => void; onCategorize?: (id: string, category: UploadableMediaCategory) => void; - onRetry?: (localId: string, signal: AbortSignal) => void; + onRetry?: (localId: string) => void; onCancel?: (localId: string) => void; /** Legacy local-only mode (table modal fallback). */ onChange?: (next: MediaFile[]) => void; @@ -45,7 +44,6 @@ export function MediaUploader({ pending = false, }: MediaUploaderProps) { const inputRef = useRef(null); - const [controllers, setControllers] = useState>({}); const addFiles = (files: FileList | null) => { if (readOnly || pending || !files || files.length === 0) return; @@ -65,19 +63,11 @@ export function MediaUploader({ if (accepted.length === 0) return; if (onUpload) { - const nextControllers: Record = { ...controllers }; - const requests: MediaUploadRequest[] = accepted.map((file) => { - const localId = `local-${crypto.randomUUID()}`; - const controller = new AbortController(); - nextControllers[localId] = controller; - return { - localId, - file, - signal: controller.signal, - onProgress: () => undefined, - }; - }); - setControllers(nextControllers); + const requests: MediaUploadRequest[] = accepted.map((file) => ({ + localId: `local-${crypto.randomUUID()}`, + file, + onProgress: () => undefined, + })); onUpload(requests); return; } @@ -98,18 +88,6 @@ export function MediaUploader({ onChange?.(uploads.filter((u) => u.id !== id)); }; - const cancel = (id: string) => { - controllers[id]?.abort(); - onCancel?.(id); - }; - - const retry = (id: string) => { - controllers[id]?.abort(); - const controller = new AbortController(); - setControllers((prev) => ({ ...prev, [id]: controller })); - onRetry?.(id, controller.signal); - }; - return (
{!readOnly && ( @@ -121,8 +99,8 @@ export function MediaUploader({ pending={pending} onRemove={remove} onCategorize={onCategorize} - onRetry={onRetry ? retry : undefined} - onCancel={cancel} + onRetry={onRetry} + onCancel={onCancel} />
); diff --git a/src/app/(protected)/workorders/_hooks/use-media-upload-session.ts b/src/app/(protected)/workorders/_hooks/use-media-upload-session.ts index 69660f14..d43b64c9 100644 --- a/src/app/(protected)/workorders/_hooks/use-media-upload-session.ts +++ b/src/app/(protected)/workorders/_hooks/use-media-upload-session.ts @@ -1,4 +1,4 @@ -import { useMemo, useState } from "react"; +import { useMemo, useRef, useState } from "react"; import type { MediaUploadRequest } from "@/app/(protected)/workorders/_components/media/media-uploader"; import { boardMediaItemsToMediaFiles } from "@/domain/work-orders/mappers/work-order-board-detail-mapper"; import type { MediaFile } from "@/domain/work-orders/types/work-order-media"; @@ -13,7 +13,13 @@ type UploadMutate = (input: { onProgress?: (percent: number) => void; }) => Promise; -function toLocalUpload(request: MediaUploadRequest): LocalUpload { +type SessionUploadRequest = { + localId: string; + file: File; + signal: AbortSignal; +}; + +function toLocalUpload(request: SessionUploadRequest): LocalUpload { const classified = classifyUpload(request.file); return { id: request.localId, @@ -29,7 +35,7 @@ function toLocalUpload(request: MediaUploadRequest): LocalUpload { }; } -function isSameAttempt(item: LocalUpload, request: MediaUploadRequest) { +function isSameAttempt(item: LocalUpload, request: SessionUploadRequest) { return item.id === request.localId && item.signal === request.signal; } @@ -38,6 +44,7 @@ export function useMediaUploadSession( uploadMutateAsync: UploadMutate, ) { const [localUploads, setLocalUploads] = useState([]); + const controllersRef = useRef(new Map()); const serverUploads = useMemo( () => boardMediaItemsToMediaFiles(detail?.media ?? []), @@ -52,10 +59,27 @@ export function useMediaUploadSession( return [...serverUploads, ...pendingLocals.filter((item) => !serverIds.has(item.id))]; }, [serverUploads, localUploads]); + function clearController(localId: string) { + controllersRef.current.delete(localId); + } + + function replaceController(localId: string): AbortController { + controllersRef.current.get(localId)?.abort(); + const controller = new AbortController(); + controllersRef.current.set(localId, controller); + return controller; + } + function startUploads(requests: MediaUploadRequest[]) { + const sessionRequests: SessionUploadRequest[] = requests.map((request) => ({ + localId: request.localId, + file: request.file, + signal: replaceController(request.localId).signal, + })); + setLocalUploads((prev) => { const next = [...prev]; - for (const request of requests) { + for (const request of sessionRequests) { const entry = toLocalUpload(request); const index = next.findIndex((item) => item.id === request.localId); if (index >= 0) { @@ -71,7 +95,7 @@ export function useMediaUploadSession( return next; }); - for (const request of requests) { + for (const request of sessionRequests) { void uploadMutateAsync({ file: request.file, signal: request.signal, @@ -84,13 +108,16 @@ export function useMediaUploadSession( }, }) .then(() => { + clearController(request.localId); setLocalUploads((prev) => prev.filter((item) => !isSameAttempt(item, request))); }) .catch((error: Error) => { if (error.name === "AbortError") { + clearController(request.localId); setLocalUploads((prev) => prev.filter((item) => !isSameAttempt(item, request))); return; } + // Keep controller entry only while retryable; replaceController on retry. setLocalUploads((prev) => prev.map((item) => isSameAttempt(item, request) @@ -107,20 +134,32 @@ export function useMediaUploadSession( } } - function retryUpload(localId: string, signal: AbortSignal) { + function retryUpload(localId: string) { const target = localUploads.find((item) => item.id === localId); if (!target?.sourceFile) return; startUploads([ { localId, file: target.sourceFile, - signal, onProgress: () => undefined, }, ]); } + function cancelUpload(localId: string) { + controllersRef.current.get(localId)?.abort(); + clearController(localId); + setLocalUploads((prev) => { + const target = prev.find((item) => item.id === localId); + if (target?.url.startsWith("blob:")) { + URL.revokeObjectURL(target.url); + } + return prev.filter((item) => item.id !== localId); + }); + } + function removeLocal(id: string) { + clearController(id); setLocalUploads((prev) => prev.filter((item) => item.id !== id)); } @@ -128,5 +167,5 @@ export function useMediaUploadSession( return localUploads.some((item) => item.id === id); } - return { uploads, startUploads, retryUpload, removeLocal, isLocal }; + return { uploads, startUploads, retryUpload, cancelUpload, removeLocal, isLocal }; } diff --git a/src/domain/work-orders/mappers/work-order-mapper.ts b/src/domain/work-orders/mappers/work-order-mapper.ts index 48535631..2b61364c 100644 --- a/src/domain/work-orders/mappers/work-order-mapper.ts +++ b/src/domain/work-orders/mappers/work-order-mapper.ts @@ -397,17 +397,6 @@ export function mapWorkOrderFormToUpdateFormData( return formData; } -/** Partial EditWorkorder payload — only Id + CompletedDate (avoids legacy GetWorkorderById). */ -export function mapCompletedDateUpdateFormData( - id: string | number, - completedDate: string, -): FormData { - const formData = new FormData(); - formData.append("Id", String(id)); - appendIfPresent(formData, "CompletedDate", completedDate); - return formData; -} - export function mapLocationOption(raw: unknown): LocationOption { const item = asRecord(raw); const name = readString(item, "name", "Name", "locationName", "LocationName"); diff --git a/src/test/app/(protected)/workorders/comp-doc-dialog-completion-contract.test.tsx b/src/test/app/(protected)/workorders/comp-doc-dialog-completion-contract.test.tsx index 41361709..5c10df5c 100644 --- a/src/test/app/(protected)/workorders/comp-doc-dialog-completion-contract.test.tsx +++ b/src/test/app/(protected)/workorders/comp-doc-dialog-completion-contract.test.tsx @@ -170,4 +170,28 @@ describe("CompDocDialog Save Draft / Generate completion contract", () => { expect(onSave).not.toHaveBeenCalled(); expect(screen.getByRole("button", { name: /^generate$/i })).toBeInTheDocument(); }); + + it("Generate never writes docStatus Yes after a successful status-capable save when date fails first", async () => { + // Regression for the prior order (status then date): if date fails, Yes must not have been sent. + // With prerequisites-first ordering, date failure short-circuits before onSave("Yes"). + const callOrder: string[] = []; + onDateCompletedChange.mockImplementation(async () => { + callOrder.push("date"); + throw new Error("completedDate conflict"); + }); + onSave.mockImplementation(async () => { + callOrder.push("status"); + }); + + const row = baseRow({ completedDate: "2026-07-01" }); + renderDialog(row); + changeDateCompleted("2026-07-20"); + fireEvent.click(screen.getByRole("button", { name: /^generate$/i })); + + await waitFor(() => { + expect(onDateCompletedChange).toHaveBeenCalled(); + }); + expect(callOrder).toEqual(["date"]); + expect(onSave).not.toHaveBeenCalled(); + }); }); diff --git a/src/test/app/(protected)/workorders/use-media-upload-session.test.tsx b/src/test/app/(protected)/workorders/use-media-upload-session.test.tsx index d0766fe7..e93ae50a 100644 --- a/src/test/app/(protected)/workorders/use-media-upload-session.test.tsx +++ b/src/test/app/(protected)/workorders/use-media-upload-session.test.tsx @@ -20,7 +20,6 @@ describe("useMediaUploadSession retry / cancel", () => { .mockImplementation(() => new Promise(() => undefined)); const { result } = renderHook(() => useMediaUploadSession(undefined, uploadMutateAsync)); - const firstController = new AbortController(); const localId = "local-1"; const file = makeFile(); @@ -29,7 +28,6 @@ describe("useMediaUploadSession retry / cancel", () => { { localId, file, - signal: firstController.signal, onProgress: () => undefined, }, ]); @@ -39,6 +37,8 @@ describe("useMediaUploadSession retry / cancel", () => { expect(result.current.uploads).toHaveLength(1); }); + const firstSignal = uploadMutateAsync.mock.calls[0]?.[0]?.signal as AbortSignal; + act(() => { rejectFirst(new Error("network")); }); @@ -47,9 +47,8 @@ describe("useMediaUploadSession retry / cancel", () => { expect(result.current.uploads[0]?.uploadStatus).toBe("failed"); }); - const retryController = new AbortController(); act(() => { - result.current.retryUpload(localId, retryController.signal); + result.current.retryUpload(localId); }); await waitFor(() => { @@ -58,11 +57,15 @@ describe("useMediaUploadSession retry / cancel", () => { expect(result.current.uploads[0]?.uploadStatus).toBe("uploading"); }); expect(uploadMutateAsync).toHaveBeenCalledTimes(2); - expect(uploadMutateAsync.mock.calls[1]?.[0]?.signal).toBe(retryController.signal); + 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("cancel after retry aborts the replacement upload and removes the local row", async () => { + 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( @@ -73,7 +76,8 @@ describe("useMediaUploadSession retry / cancel", () => { ) .mockImplementation( ({ signal }: { signal?: AbortSignal }) => - new Promise((_resolve, reject) => { + new Promise((resolve, reject) => { + resolveRetry = resolve; signal?.addEventListener("abort", () => { const error = new Error("aborted"); error.name = "AbortError"; @@ -91,7 +95,6 @@ describe("useMediaUploadSession retry / cancel", () => { { localId, file, - signal: new AbortController().signal, onProgress: () => undefined, }, ]); @@ -105,25 +108,28 @@ describe("useMediaUploadSession retry / cancel", () => { expect(result.current.uploads[0]?.uploadStatus).toBe("failed"); }); - const retryController = new AbortController(); act(() => { - result.current.retryUpload(localId, retryController.signal); + 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(() => { - // Mirrors MediaUploader.cancel: abort the mapped controller, then removeLocal. - retryController.abort(); - result.current.removeLocal(localId); + result.current.cancelUpload(localId); }); await waitFor(() => { expect(result.current.uploads).toHaveLength(0); }); - expect(retryController.signal.aborted).toBe(true); - expect(uploadMutateAsync.mock.calls[1]?.[0]?.signal).toBe(retryController.signal); + 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); + }); }); }); diff --git a/src/test/domain/work-orders/mappers/work-order-form-payload.test.ts b/src/test/domain/work-orders/mappers/work-order-form-payload.test.ts index ca5d0ab7..5b71e223 100644 --- a/src/test/domain/work-orders/mappers/work-order-form-payload.test.ts +++ b/src/test/domain/work-orders/mappers/work-order-form-payload.test.ts @@ -1,6 +1,5 @@ import { describe, expect, it } from "vitest"; import { - mapCompletedDateUpdateFormData, mapWorkOrderFormToCreatePayload, mapWorkOrderFormToUpdateFormData, } from "@/domain/work-orders/mappers/work-order-mapper"; @@ -137,24 +136,3 @@ describe("mapWorkOrderFormToUpdateFormData", () => { expect(formData.getAll("CategoryIds")).toEqual([]); }); }); - -describe("mapCompletedDateUpdateFormData", () => { - it("builds FormData with only Id and CompletedDate", () => { - const formData = mapCompletedDateUpdateFormData(42, "2026-07-30"); - - expect(formData.get("Id")).toBe("42"); - expect(formData.get("CompletedDate")).toBe("2026-07-30"); - expect(formData.get("Status")).toBeNull(); - expect(formData.get("WorkerOrderTitle")).toBeNull(); - expect(formData.get("Description")).toBeNull(); - expect(formData.getAll("ContactIds")).toEqual([]); - expect(formData.getAll("CategoryIds")).toEqual([]); - }); - - it("omits empty CompletedDate", () => { - const formData = mapCompletedDateUpdateFormData(1, " "); - - expect(formData.get("Id")).toBe("1"); - expect(formData.get("CompletedDate")).toBeNull(); - }); -});