From 7e1dcb7a360abfc624168a3e23411ba20f835a63 Mon Sep 17 00:00:00 2001 From: Arthur Bassi Date: Tue, 1 Sep 2026 10:20:38 -0300 Subject: [PATCH] fix(work-orders): toast after assigned-to save on the board Show the save toast only after the board PATCH succeeds. --- .../list/table/wo-table-row-cells.tsx | 5 +- .../utils/work-order-feedback-toasts.ts | 9 ++ ...wo-table-row-cells-assigned-toast.test.tsx | 70 +++++++++++++ ...er-table-mutations-assigned-toast.test.tsx | 97 +++++++++++++++++++ .../utils/work-order-feedback-toasts.test.ts | 41 ++++++++ 5 files changed, 219 insertions(+), 3 deletions(-) create mode 100644 src/test/app/(protected)/workorders/wo-table-row-cells-assigned-toast.test.tsx create mode 100644 src/test/domain/work-orders/use-cases/use-work-order-table-mutations-assigned-toast.test.tsx diff --git a/src/app/(protected)/workorders/_components/list/table/wo-table-row-cells.tsx b/src/app/(protected)/workorders/_components/list/table/wo-table-row-cells.tsx index aac347e1..c17deae3 100644 --- a/src/app/(protected)/workorders/_components/list/table/wo-table-row-cells.tsx +++ b/src/app/(protected)/workorders/_components/list/table/wo-table-row-cells.tsx @@ -1,6 +1,7 @@ import type { CSSProperties } from "react"; import type { LocationOption, WorkOrderUser } from "@/domain/work-orders/types/work-order"; import type { WorkOrderTableRow } from "@/domain/work-orders/types/work-order-table-row"; +import { applyAssignedToTableSave } from "@/domain/work-orders/utils/work-order-feedback-toasts"; import { AssignedToCell } from "./cells/assigned-to-cell"; import { DateCell } from "./cells/date-cell"; import { DueDateCell } from "./cells/due-date-cell"; @@ -111,9 +112,7 @@ export function WoTableRowCells({ dispatcherId={row.dispatcherId} users={users} highlight={!row.dispatcherId} - onSave={(id, name) => - handlers.onPatch(row.id, { dispatcherId: id, dispatcherName: name }) - } + onSave={(id, name) => applyAssignedToTableSave(handlers.onPatch, row.id, id, name)} /> ({ + SiteCell: () => null, +})); +vi.mock( + "@/app/(protected)/workorders/_components/list/table/cells/editable-wo-number-cell", + () => ({ + EditableWONumberCell: () => null, + }), +); +vi.mock("@/app/(protected)/workorders/_components/list/table/cells/type-cell", () => ({ + TypeCell: () => null, +})); +vi.mock("@/app/(protected)/workorders/_components/list/table/cells/date-cell", () => ({ + DateCell: () => null, +})); +vi.mock("@/app/(protected)/workorders/_components/list/table/cells/due-date-cell", () => ({ + DueDateCell: () => null, +})); +vi.mock("@/app/(protected)/workorders/_components/list/table/cells/assigned-to-cell", () => ({ + AssignedToCell: ({ onSave }: { onSave: (id: string, name: string) => void }) => ( + + ), +})); + +const row = { id: 7, dispatcherId: "u1" } as WorkOrderTableRow; + +describe("WoTableRowCells assigned-to persist", () => { + it("forwards assigned-to save through patch onSuccess instead of onPatchRow", () => { + const onPatch = vi.fn(); + const onPatchRow = vi.fn(); + const handlers = { onPatch } as unknown as WoTableRowHandlers; + + renderWithProviders( + + + + + + +
, + { withAuth: false }, + ); + + fireEvent.click(screen.getByRole("button", { name: "persist-assigned" })); + + expect(onPatchRow).not.toHaveBeenCalled(); + expect(onPatch).toHaveBeenCalledWith( + 7, + { dispatcherId: "u2", dispatcherName: "Admin test" }, + { onSuccess: notifyWorkOrderSaved }, + ); + }); +}); diff --git a/src/test/domain/work-orders/use-cases/use-work-order-table-mutations-assigned-toast.test.tsx b/src/test/domain/work-orders/use-cases/use-work-order-table-mutations-assigned-toast.test.tsx new file mode 100644 index 00000000..0bfe1699 --- /dev/null +++ b/src/test/domain/work-orders/use-cases/use-work-order-table-mutations-assigned-toast.test.tsx @@ -0,0 +1,97 @@ +import type { ReactNode } from "react"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { act, renderHook, waitFor } from "@testing-library/react"; +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { toast } from "react-toastify"; +import type { WorkOrderTableRow } from "@/domain/work-orders/types/work-order-table-row"; +import { useWorkOrderTableMutations } from "@/domain/work-orders/use-cases/use-work-order-table-mutations"; +import { + applyAssignedToTableSave, + workOrderSavedMessage, +} from "@/domain/work-orders/utils/work-order-feedback-toasts"; + +const patchBoardField = vi.fn(); + +vi.mock("@/domain/work-orders/api/work-orders-api", () => ({ + workOrdersApi: { + patchBoardField: (...args: unknown[]) => patchBoardField(...args), + }, +})); + +vi.mock("react-toastify", () => ({ + toast: { error: vi.fn(), success: vi.fn(), warning: vi.fn() }, +})); + +const ROW = { + id: 7, + dispatcherId: "u1", + dispatcherName: "Admin", + rowVersion: "v1", +} as WorkOrderTableRow; + +function makeWrapper() { + const client = new QueryClient({ + defaultOptions: { queries: { retry: false }, mutations: { retry: false } }, + }); + return function Wrapper({ children }: { children: ReactNode }) { + return {children}; + }; +} + +describe("useWorkOrderTableMutations assigned-to toast", () => { + beforeEach(() => { + patchBoardField.mockReset(); + vi.mocked(toast.success).mockClear(); + vi.mocked(toast.error).mockClear(); + }); + + it("toasts assigned-to save only after the board PATCH succeeds", async () => { + let release: () => void = () => undefined; + const gate = new Promise((resolve) => { + release = resolve; + }); + patchBoardField.mockImplementation(async () => { + await gate; + return { ...ROW, dispatcherId: "u2", dispatcherName: "Admin test", rowVersion: "v2" }; + }); + + const { result } = renderHook( + () => + useWorkOrderTableMutations({ + onPatch: () => undefined, + clearPatch: () => undefined, + getRow: () => ROW, + }), + { wrapper: makeWrapper() }, + ); + + act(() => { + applyAssignedToTableSave(result.current.patchField, 7, "u2", "Admin test"); + }); + + expect(toast.success).not.toHaveBeenCalled(); + release(); + await waitFor(() => expect(toast.success).toHaveBeenCalledWith(workOrderSavedMessage())); + }); + + it("does not toast assigned-to save when the board PATCH fails", async () => { + patchBoardField.mockRejectedValue(new Error("Failed to update work order")); + + const { result } = renderHook( + () => + useWorkOrderTableMutations({ + onPatch: () => undefined, + clearPatch: () => undefined, + getRow: () => ROW, + }), + { wrapper: makeWrapper() }, + ); + + act(() => { + applyAssignedToTableSave(result.current.patchField, 7, "u2", "Admin test"); + }); + + await waitFor(() => expect(toast.error).toHaveBeenCalled()); + expect(toast.success).not.toHaveBeenCalled(); + }); +}); diff --git a/src/test/domain/work-orders/utils/work-order-feedback-toasts.test.ts b/src/test/domain/work-orders/utils/work-order-feedback-toasts.test.ts index 643a02dc..e9a5483f 100644 --- a/src/test/domain/work-orders/utils/work-order-feedback-toasts.test.ts +++ b/src/test/domain/work-orders/utils/work-order-feedback-toasts.test.ts @@ -2,10 +2,12 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; import { toast } from "react-toastify"; import { toVendorTablePatch } from "@/domain/work-orders/utils/vendor-assignment-patch"; import { + applyAssignedToTableSave, applyVendorTableSave, isVendorAssignmentPatch, notifySlideOverSaveSuccess, notifyVendorAssigned, + notifyWorkOrderSaved, patchWorkOrderAsCanceled, patchWorkOrderAsCompleted, vendorAssignedMessage, @@ -137,6 +139,45 @@ describe("work-order-feedback-toasts", () => { expect(toast.success).not.toHaveBeenCalledWith(vendorAssignedMessage()); }); + it("toasts generic save after assigned-to persist onSuccess", () => { + const onPatch = vi.fn(); + applyAssignedToTableSave(onPatch, 7, "u2", "Admin test"); + + expect(onPatch).toHaveBeenCalledWith( + 7, + { dispatcherId: "u2", dispatcherName: "Admin test" }, + { onSuccess: notifyWorkOrderSaved }, + ); + expect(toast.success).not.toHaveBeenCalled(); + + const { onSuccess } = onPatch.mock.calls[0][2] as { onSuccess: () => void }; + onSuccess(); + expect(toast.success).toHaveBeenCalledWith(workOrderSavedMessage()); + expect(toast.warning).not.toHaveBeenCalled(); + }); + + it("toasts the same save copy after unassign persist onSuccess", () => { + const onPatch = vi.fn(); + applyAssignedToTableSave(onPatch, 7, "", ""); + + expect(onPatch).toHaveBeenCalledWith( + 7, + { dispatcherId: "", dispatcherName: "" }, + { onSuccess: notifyWorkOrderSaved }, + ); + expect(toast.success).not.toHaveBeenCalled(); + + const { onSuccess } = onPatch.mock.calls[0][2] as { onSuccess: () => void }; + onSuccess(); + expect(toast.success).toHaveBeenCalledWith(workOrderSavedMessage()); + }); + + it("does not toast assigned-to save when persist onSuccess is skipped", () => { + const onPatch = vi.fn(); + applyAssignedToTableSave(onPatch, 7, "u2", "Admin test"); + expect(toast.success).not.toHaveBeenCalled(); + }); + it("completes a work order without a success toast", () => { const patchField = vi.fn(); patchWorkOrderAsCompleted(patchField, 42);