shoc-frontend-new/src/test/app/(protected)/workorders/use-slide-over-edit-state-completed.test.tsx
Alexandre Brandizzi f9b5d3e0ea fix(workorders): save from the detail panel after the order leaves the list
The board patch read the work order version from the loaded list only. Once a
save moved the order out of that list (e.g. to another week), the next save,
Cancel or Complete from the open panel failed for lack of a version. The panel
now passes its server row as the fallback patch base.
2026-10-01 23:05:14 -03:00

237 lines
6.2 KiB
TypeScript

import { act, renderHook } from "@testing-library/react";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { toast } from "react-toastify";
import { useSlideOverEditState } from "@/app/(protected)/workorders/_components/detail/use-slide-over-edit-state";
import type { SlideOverTab } from "@/app/(protected)/workorders/_components/detail/use-work-order-slide-over";
import type { WorkOrderTableRow } from "@/domain/work-orders/types/work-order-table-row";
import {
vendorAssignedMessage,
workOrderSavedMessage,
} from "@/domain/work-orders/utils/work-order-feedback-toasts";
vi.mock("react-toastify", () => ({
toast: {
warning: vi.fn(),
success: vi.fn(),
error: vi.fn(),
},
}));
function baseRow(overrides: Partial<WorkOrderTableRow> = {}): WorkOrderTableRow {
return {
id: 100,
woNumber: "100",
site: "SITE",
type: "Emergency",
severity: null,
dispatcherId: "d1",
dispatcherName: "Alex",
scheduledOn: "2026-07-16",
completedDate: "",
targetWeek: "",
dayGroup: "2026-07-16",
dayLabel: "Thu",
location: "Main",
locationId: 5,
pm: "",
serviceNotes: "",
extraServices: [],
vendorId: "v1",
company: "Acme",
tech: "Sam",
techPhone: "",
apptStart: "",
apptEnd: "",
apptTime: "",
status: "Incomplete",
docStatus: "",
pocName: "",
pocPhone: "",
pocNotes: "",
additionalContacts: [],
dueDate: "",
rescheduleCount: 0,
carriedOver: 0,
originalWeek: "",
originalDate: "",
isPastDue: false,
scheduleWeekOnly: false,
mediaCount: 0,
title: "",
priority: "",
primaryDispatchId: null,
rowVersion: "1",
dispatchRowVersion: "",
flagColor: null,
isAddOn: false,
upliftSummary: { hasUplift: false, pendingCount: 0 },
...overrides,
};
}
describe("useSlideOverEditState completed lock", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("leaves edit mode and refuses commit when the same work order becomes completed", () => {
const onSave = vi.fn();
const onClose = vi.fn();
const setTab = vi.fn();
const open = baseRow();
const { result, rerender } = renderHook(
({ row, infoSource }) =>
useSlideOverEditState({
row,
editMode: true,
infoSource,
onSave,
onClose,
closeDisabled: false,
setTab,
}),
{ initialProps: { row: open, infoSource: open } },
);
expect(result.current.editing).toBe(true);
act(() => {
result.current.handleDraftChange({ woNumber: "WO-LOCKED" });
});
const completed = baseRow({ status: "Completed", woNumber: "100" });
rerender({ row: completed, infoSource: completed });
expect(result.current.editing).toBe(false);
act(() => {
result.current.commit();
});
expect(onSave).not.toHaveBeenCalled();
});
it("applies a repeated same-row tab request without resetting the draft", () => {
const open = baseRow();
const setTab = vi.fn();
const { result, rerender } = renderHook(
({ initialTab, openRequestKey }) =>
useSlideOverEditState({
row: open,
editMode: false,
infoSource: open,
onSave: vi.fn(),
onClose: vi.fn(),
closeDisabled: false,
setTab,
initialTab,
openRequestKey,
}),
{ initialProps: { initialTab: "info" as SlideOverTab, openRequestKey: 1 } },
);
act(() => {
result.current.handleDraftChange({ woNumber: "edited" });
});
rerender({ initialTab: "extras", openRequestKey: 2 });
expect(setTab).toHaveBeenLastCalledWith("extras");
expect(result.current.activeDraft?.woNumber).toBe("edited");
});
it("toasts vendor assigned after a successful save that includes vendorId", () => {
const onSave = vi.fn();
const open = baseRow();
const { result } = renderHook(() =>
useSlideOverEditState({
row: open,
editMode: true,
infoSource: open,
onSave,
onClose: vi.fn(),
closeDisabled: false,
setTab: vi.fn(),
}),
);
act(() => {
result.current.handleDraftChange({ vendorId: "v2", company: "New Co", tech: "Lee" });
});
act(() => {
result.current.commit();
});
expect(onSave).toHaveBeenCalled();
expect(toast.success).not.toHaveBeenCalled();
const options = onSave.mock.calls[0][2] as { onSuccess: () => void };
act(() => {
options.onSuccess();
});
expect(toast.success).toHaveBeenCalledWith(vendorAssignedMessage());
expect(toast.success).toHaveBeenCalledTimes(1);
});
it("toasts generic save after a successful non-vendor slide-over patch", () => {
const onSave = vi.fn();
const open = baseRow();
const { result } = renderHook(() =>
useSlideOverEditState({
row: open,
editMode: true,
infoSource: open,
onSave,
onClose: vi.fn(),
closeDisabled: false,
setTab: vi.fn(),
}),
);
act(() => {
result.current.handleDraftChange({ dispatcherName: "Blair" });
});
act(() => {
result.current.commit();
});
const options = onSave.mock.calls[0][2] as { onSuccess: () => void };
act(() => {
options.onSuccess();
});
expect(toast.success).toHaveBeenCalledWith(workOrderSavedMessage());
expect(toast.success).not.toHaveBeenCalledWith(vendorAssignedMessage());
});
it("sends the panel's server row so a save works after the work order left the list", () => {
const onSave = vi.fn();
const opened = baseRow({ rowVersion: "opened" });
const fresh = baseRow({ scheduledOn: "2026-07-22", rowVersion: "after-first-save" });
const { result } = renderHook(() =>
useSlideOverEditState({
row: opened,
editMode: true,
infoSource: fresh,
onSave,
onClose: vi.fn(),
closeDisabled: false,
setTab: vi.fn(),
}),
);
act(() => {
result.current.handleDraftChange({ dispatcherName: "Blair" });
});
act(() => {
result.current.commit();
});
expect(onSave).toHaveBeenCalledWith(
100,
expect.anything(),
expect.objectContaining({ fallbackRow: fresh }),
);
});
});