import { fireEvent, screen, waitFor } from "@testing-library/react"; import { afterEach, describe, expect, it, vi } from "vitest"; import { SignoffSection } from "@/app/v/[token]/dispatch/_components/signoff-section"; import { vendorPortalApi } from "@/domain/vendor-portal/api/vendor-portal-api"; import { createAppQueryClient } from "@/lib/query/query-client"; import { renderWithProviders } from "@/test/test-utils"; const toastMocks = vi.hoisted(() => ({ success: vi.fn(), error: vi.fn() })); vi.mock("react-toastify", () => ({ toast: toastMocks })); afterEach(() => { vi.restoreAllMocks(); toastMocks.success.mockReset(); toastMocks.error.mockReset(); }); function renderSection( overrides: Partial> = {}, options: { useAppQueryClient?: boolean } = {}, ) { return renderWithProviders( , { withAuth: false, ...(options.useAppQueryClient ? { queryClient: createAppQueryClient() } : {}), }, ); } function submitTypedSignoff(name = "Jane Doe") { fireEvent.click(screen.getByRole("radio", { name: "Typed" })); const [nameInput, signatureInput] = screen.getAllByRole("textbox"); fireEvent.change(nameInput, { target: { value: name } }); fireEvent.change(signatureInput, { target: { value: name } }); fireEvent.click(screen.getByRole("button", { name: "Submit Signoff" })); } describe("SignoffSection", () => { it("shows status-neutral locked copy when submission is locked", () => { renderSection({ submissionLocked: true }); expect(screen.getByText(/Signoff is unavailable for this dispatch/i)).toBeInTheDocument(); expect(screen.queryByText(/refused/i)).not.toBeInTheDocument(); expect(screen.queryByRole("button", { name: "Submit Signoff" })).not.toBeInTheDocument(); }); it("shows the submission form when unlocked", () => { renderSection(); expect(screen.getByRole("button", { name: "Submit Signoff" })).toBeInTheDocument(); }); it("shows an existing signoff even while locked", () => { renderSection({ signoffType: "vendor", nameLabel: "Your name", submissionLocked: true, existing: [{ signoffType: "vendor", name: "Grace Hopper", signatureMethod: "typed" }], }); expect(screen.getByText("Grace Hopper")).toBeInTheDocument(); expect(screen.queryByText(/Signoff is unavailable/i)).not.toBeInTheDocument(); expect(screen.queryByRole("button", { name: "Submit Signoff" })).not.toBeInTheDocument(); }); it("requires a name and signature before submitting", () => { const addSignoff = vi.spyOn(vendorPortalApi, "addSignoff"); renderSection(); fireEvent.click(screen.getByRole("button", { name: "Submit Signoff" })); expect(screen.getByText("Name and signature are required.")).toBeInTheDocument(); expect(addSignoff).not.toHaveBeenCalled(); }); it("submits a typed signoff and calls onAdded", async () => { vi.spyOn(vendorPortalApi, "addSignoff").mockResolvedValue({ signoffType: "customer", name: "Jane Doe", signatureMethod: "typed", }); const onAdded = vi.fn(); renderSection({ onAdded }); submitTypedSignoff(); await waitFor(() => expect(onAdded).toHaveBeenCalled()); expect(vendorPortalApi.addSignoff).toHaveBeenCalledWith("portal-token", 9, { name: "Jane Doe", signature: "Jane Doe", signatureMethod: "typed", signoffType: "customer", }); }); it("shows an error message when the submission fails", async () => { vi.spyOn(vendorPortalApi, "addSignoff").mockRejectedValue(new Error("boom")); renderSection(); submitTypedSignoff(); expect( await screen.findByText("Unable to submit the signoff. Please try again."), ).toBeInTheDocument(); }); it("shows only the inline error on a failed submit, not a duplicate global toast", async () => { vi.spyOn(vendorPortalApi, "addSignoff").mockRejectedValue(new Error("boom")); renderSection({}, { useAppQueryClient: true }); submitTypedSignoff(); expect( await screen.findByText("Unable to submit the signoff. Please try again."), ).toBeInTheDocument(); expect(toastMocks.error).not.toHaveBeenCalled(); }); });