mirror of
https://github.com/Sea-Haven-Industries/shoc-frontend-new.git
synced 2026-10-02 21:33:50 +00:00
All six vendor-portal dispatch mutation hooks lacked meta.suppressErrorToast, so a failed checklist toggle, signoff, comment, or uplift request/withdraw/ revise raised both the global MutationCache toast (leaking the raw server message) and the section's own inline error, contradicting this ticket's "no behavior change" goal versus main's try/catch-only handling. Also fixes a related gap in checklist-section.tsx: updateItem.isError only reflects the latest call on the shared mutation observer, so a failed toggle whose item was not the most recent click showed no error at all. Tracks failure locally instead, mirroring main's per-call error state. Added regression coverage with the app's real query client (createAppQueryClient) for comments, signoff, and uplift withdraw, asserting the inline message shows and the global toast does not fire; plus a mixed pass/fail concurrent-toggle case for the checklist. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
126 lines
4.3 KiB
TypeScript
126 lines
4.3 KiB
TypeScript
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<React.ComponentProps<typeof SignoffSection>> = {},
|
|
options: { useAppQueryClient?: boolean } = {},
|
|
) {
|
|
return renderWithProviders(
|
|
<SignoffSection
|
|
token="portal-token"
|
|
dispatchId={9}
|
|
existing={[]}
|
|
signoffType="customer"
|
|
nameLabel="Customer name"
|
|
onAdded={vi.fn()}
|
|
{...overrides}
|
|
/>,
|
|
{
|
|
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();
|
|
});
|
|
});
|