mirror of
https://github.com/Sea-Haven-Industries/shoc-frontend-new.git
synced 2026-10-02 05:03:12 +00:00
271 lines
9 KiB
TypeScript
271 lines
9 KiB
TypeScript
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> = {}): 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<Partial<WorkOrderTableRow>> = [];
|
|
const client = new QueryClient({
|
|
defaultOptions: { queries: { retry: false }, mutations: { retry: false } },
|
|
});
|
|
|
|
render(
|
|
<QueryClientProvider client={client}>
|
|
<CompDocDialog
|
|
row={row}
|
|
open
|
|
onOpenChange={vi.fn()}
|
|
onSave={async (data, status) => {
|
|
await onSave(data, status);
|
|
patches.push({ docStatus: status });
|
|
}}
|
|
onDateCompletedChange={async (date) => {
|
|
await onDateCompletedChange(date);
|
|
patches.push({ completedDate: date });
|
|
}}
|
|
/>
|
|
</QueryClientProvider>,
|
|
);
|
|
|
|
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(
|
|
<QueryClientProvider client={client}>
|
|
<CompDocDialog
|
|
row={row}
|
|
open
|
|
onOpenChange={vi.fn()}
|
|
onSave={onSave}
|
|
onDateCompletedChange={onDateCompletedChange}
|
|
/>
|
|
</QueryClientProvider>,
|
|
);
|
|
|
|
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();
|
|
});
|
|
});
|