import { fireEvent, render, screen, waitFor } from "@testing-library/react"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { toast } from "react-toastify"; import { CompDocDialog } from "@/app/(protected)/workorders/_components/comp-doc/comp-doc-dialog"; import type { WorkOrderTableRow } from "@/domain/work-orders/types/work-order-table-row"; vi.mock("@/domain/locations/use-cases/use-location-detail", () => ({ useLocationDetail: () => ({ data: undefined }), })); vi.mock("@/domain/work-orders/use-cases/use-work-order-detail", () => ({ useWorkOrderBoardDetail: () => ({ data: undefined, isLoading: false }), })); vi.mock("@/domain/work-orders/use-cases/use-work-order-mutations", () => ({ useUploadCompletionDoc: () => ({ mutate: vi.fn(), isPending: false }), })); vi.mock("react-toastify", () => ({ toast: { error: vi.fn(), success: vi.fn() }, })); vi.mock("@/app/(protected)/workorders/_components/comp-doc/comp-doc-draft-toast", () => ({ showCompDocDraftSavedToast: vi.fn(), })); const SCHEDULED_ON = "2026-06-01"; function baseRow(overrides: Partial = {}): WorkOrderTableRow { const defaults: WorkOrderTableRow = { id: 42, woNumber: "WO-42", site: "SEA1", type: "PM", severity: null, dispatcherId: "u1", dispatcherName: "Dispatcher", scheduledOn: SCHEDULED_ON, completedDate: "", targetWeek: "", dayGroup: "", dayLabel: "", location: "Seattle", locationId: "mock-loc-1", pm: "HVAC", serviceNotes: "", extraServices: [], vendorId: "", company: "Vendor Co", tech: "Tech One", techPhone: "", apptStart: "", apptEnd: "", apptTime: "", status: "Scheduled", docStatus: "No", pocName: "Amazon Rep", pocPhone: "", pocNotes: "", dueDate: "", rescheduleCount: 0, carriedOver: 0, originalWeek: "", originalDate: "", isPastDue: false, scheduleWeekOnly: false, mediaCount: 0, title: "PM — SEA1", priority: "Sev 3", primaryDispatchId: null, rowVersion: "", dispatchRowVersion: "", flagColor: null, isAddOn: false, }; return { ...defaults, ...overrides, severity: overrides.severity !== undefined ? overrides.severity : defaults.severity, }; } describe("CompDocDialog Save Draft / Generate completion contract", () => { const onSave = vi.fn(); const onDateCompletedChange = vi.fn(); beforeEach(() => { onSave.mockReset(); onDateCompletedChange.mockReset(); vi.mocked(toast.error).mockReset(); }); function renderDialog(row: WorkOrderTableRow) { // Mirrors board wiring: only completedDate / docStatus — never scheduledOn or lifecycle. const patches: Array> = []; const client = new QueryClient({ defaultOptions: { queries: { retry: false }, mutations: { retry: false } }, }); render( { await onSave(data, status); patches.push({ docStatus: status }); }} onDateCompletedChange={async (date) => { await onDateCompletedChange(date); patches.push({ completedDate: date }); }} /> , ); return { patches }; } function changeDateCompleted(value: string) { const dateInput = document.querySelector('input[type="date"]'); expect(dateInput).not.toBeNull(); fireEvent.change(dateInput!, { target: { value } }); } it("Save draft emits completedDate without writing scheduledOn or lifecycle Completed", async () => { const row = baseRow(); const { patches } = renderDialog(row); changeDateCompleted("2026-07-15"); fireEvent.click(screen.getByRole("button", { name: /save draft/i })); await waitFor(() => { expect(onDateCompletedChange).toHaveBeenCalledWith("2026-07-15"); }); expect(onSave).toHaveBeenCalledWith(expect.any(Object), "No"); expect(patches).toEqual([{ completedDate: "2026-07-15" }, { docStatus: "No" }]); expect(patches.some((patch) => "scheduledOn" in patch)).toBe(false); expect(patches.some((patch) => patch.status === "Completed")).toBe(false); expect(row.scheduledOn).toBe(SCHEDULED_ON); expect(row.status).toBe("Scheduled"); }); it("Generate emits completedDate before doc Yes without auto-setting lifecycle Completed", async () => { const row = baseRow({ completedDate: "2026-07-01" }); const { patches } = renderDialog(row); changeDateCompleted("2026-07-20"); fireEvent.click(screen.getByRole("button", { name: /^generate$/i })); await waitFor(() => { expect(onDateCompletedChange).toHaveBeenCalledWith("2026-07-20"); }); expect(onSave).toHaveBeenCalledWith( expect.objectContaining({ generatedAt: expect.any(String) }), "Yes", ); expect(patches).toEqual([{ completedDate: "2026-07-20" }, { docStatus: "Yes" }]); expect(patches.some((patch) => "scheduledOn" in patch)).toBe(false); expect(patches.some((patch) => patch.status === "Completed")).toBe(false); expect(row.scheduledOn).toBe(SCHEDULED_ON); expect(row.status).toBe("Scheduled"); }); it("Generate keeps Draft and skips docStatus Yes when Date Work Completed is blank", async () => { const row = baseRow({ completedDate: "" }); renderDialog(row); fireEvent.click(screen.getByRole("button", { name: /^generate$/i })); await waitFor(() => { expect(toast.error).toHaveBeenCalledWith( "Date Work Completed is required to generate the document.", ); }); expect(onDateCompletedChange).not.toHaveBeenCalled(); expect(onSave).not.toHaveBeenCalled(); expect(screen.getByRole("button", { name: /^generate$/i })).toBeInTheDocument(); }); it("Generate keeps Draft and skips docStatus Yes when completedDate patch fails", async () => { const row = baseRow({ completedDate: "2026-07-01" }); const client = new QueryClient({ defaultOptions: { queries: { retry: false }, mutations: { retry: false } }, }); const invalidateSpy = vi.spyOn(client, "invalidateQueries"); onDateCompletedChange.mockRejectedValueOnce(new Error("conflict")); render( , ); changeDateCompleted("2026-07-20"); fireEvent.click(screen.getByRole("button", { name: /^generate$/i })); await waitFor(() => { expect(onDateCompletedChange).toHaveBeenCalledWith("2026-07-20"); }); expect(onSave).not.toHaveBeenCalled(); expect(screen.getByRole("button", { name: /^generate$/i })).toBeInTheDocument(); await waitFor(() => { expect(invalidateSpy).toHaveBeenCalled(); }); }); 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(); }); it("Save draft persists PM comments notes through onSave", async () => { const row = baseRow(); renderDialog(row); const notes = screen.getByPlaceholderText(/add findings/i); fireEvent.change(notes, { target: { value: "No issues noted" } }); fireEvent.click(screen.getByRole("button", { name: /save draft/i })); await waitFor(() => { expect(onSave).toHaveBeenCalledWith( expect.objectContaining({ notes: "No issues noted" }), "No", ); }); }); it("Mark as not required rolls back UI when persist fails", async () => { onSave.mockRejectedValueOnce(new Error("persist failed")); const row = baseRow(); renderDialog(row); fireEvent.click(screen.getByRole("button", { name: /mark as not required/i })); await waitFor(() => { expect(onSave).toHaveBeenCalledWith(expect.any(Object), "NN"); }); await waitFor(() => { expect(screen.getByRole("button", { name: /mark as not required/i })).toBeInTheDocument(); }); expect(screen.queryByRole("button", { name: /^undo$/i })).not.toBeInTheDocument(); }); });