refactor: move vendor-portal dispatch actions and 2 pages onto proper data hooks
Two groups of components were calling the API directly instead of using
this codebase's standard React Query hooks, with no shared caching and
duplicated submit/error-handling boilerplate.
- checklist-section.tsx, comments-section.tsx, signoff-section.tsx,
uplift-requests-section.tsx: replace direct vendorPortalApi calls and
hand-rolled submitting/error state with mutation hooks in the new
use-vendor-portal-dispatch-actions.ts. uplift-requests-section keeps
its existing multi-step evidence-upload orchestration intact and only
swaps the 3 raw API calls for mutateAsync, since restructuring that
flow around declarative mutation callbacks carried more risk than
value. Caught and fixed a real bug along the way: two components were
passing a prop callback directly as a mutation's onSuccess, which
would have forwarded React Query's extra callback arguments into it.
- followups/index.tsx, vendors/preferences.tsx: split from a single
file doing state, API calls, and rendering into a hook (owns data
and handlers) plus small presentational components, matching the
task-templates refactor pattern. Added a missing centralized query
key (vendorOperations.sitePreferences) that preferences.tsx was
building as a raw inline array.
- use-list-page-filters.ts: small shared hook for search/page/pageSize
state, used by the new followups hook.
29 new/updated tests across all touched files; none of these had
coverage for their mutation/error paths before.
2026-09-25 10:53:23 -05:00
|
|
|
import { fireEvent, screen, waitFor } from "@testing-library/react";
|
|
|
|
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
2026-08-10 18:25:57 -03:00
|
|
|
import { SignoffSection } from "@/app/v/[token]/dispatch/_components/signoff-section";
|
refactor: move vendor-portal dispatch actions and 2 pages onto proper data hooks
Two groups of components were calling the API directly instead of using
this codebase's standard React Query hooks, with no shared caching and
duplicated submit/error-handling boilerplate.
- checklist-section.tsx, comments-section.tsx, signoff-section.tsx,
uplift-requests-section.tsx: replace direct vendorPortalApi calls and
hand-rolled submitting/error state with mutation hooks in the new
use-vendor-portal-dispatch-actions.ts. uplift-requests-section keeps
its existing multi-step evidence-upload orchestration intact and only
swaps the 3 raw API calls for mutateAsync, since restructuring that
flow around declarative mutation callbacks carried more risk than
value. Caught and fixed a real bug along the way: two components were
passing a prop callback directly as a mutation's onSuccess, which
would have forwarded React Query's extra callback arguments into it.
- followups/index.tsx, vendors/preferences.tsx: split from a single
file doing state, API calls, and rendering into a hook (owns data
and handlers) plus small presentational components, matching the
task-templates refactor pattern. Added a missing centralized query
key (vendorOperations.sitePreferences) that preferences.tsx was
building as a raw inline array.
- use-list-page-filters.ts: small shared hook for search/page/pageSize
state, used by the new followups hook.
29 new/updated tests across all touched files; none of these had
coverage for their mutation/error paths before.
2026-09-25 10:53:23 -05:00
|
|
|
import { vendorPortalApi } from "@/domain/vendor-portal/api/vendor-portal-api";
|
|
|
|
|
import { renderWithProviders } from "@/test/test-utils";
|
|
|
|
|
|
|
|
|
|
afterEach(() => {
|
|
|
|
|
vi.restoreAllMocks();
|
|
|
|
|
});
|
2026-08-10 18:25:57 -03:00
|
|
|
|
|
|
|
|
function renderSection(overrides: Partial<React.ComponentProps<typeof SignoffSection>> = {}) {
|
refactor: move vendor-portal dispatch actions and 2 pages onto proper data hooks
Two groups of components were calling the API directly instead of using
this codebase's standard React Query hooks, with no shared caching and
duplicated submit/error-handling boilerplate.
- checklist-section.tsx, comments-section.tsx, signoff-section.tsx,
uplift-requests-section.tsx: replace direct vendorPortalApi calls and
hand-rolled submitting/error state with mutation hooks in the new
use-vendor-portal-dispatch-actions.ts. uplift-requests-section keeps
its existing multi-step evidence-upload orchestration intact and only
swaps the 3 raw API calls for mutateAsync, since restructuring that
flow around declarative mutation callbacks carried more risk than
value. Caught and fixed a real bug along the way: two components were
passing a prop callback directly as a mutation's onSuccess, which
would have forwarded React Query's extra callback arguments into it.
- followups/index.tsx, vendors/preferences.tsx: split from a single
file doing state, API calls, and rendering into a hook (owns data
and handlers) plus small presentational components, matching the
task-templates refactor pattern. Added a missing centralized query
key (vendorOperations.sitePreferences) that preferences.tsx was
building as a raw inline array.
- use-list-page-filters.ts: small shared hook for search/page/pageSize
state, used by the new followups hook.
29 new/updated tests across all touched files; none of these had
coverage for their mutation/error paths before.
2026-09-25 10:53:23 -05:00
|
|
|
return renderWithProviders(
|
2026-08-10 18:25:57 -03:00
|
|
|
<SignoffSection
|
|
|
|
|
token="portal-token"
|
|
|
|
|
dispatchId={9}
|
|
|
|
|
existing={[]}
|
|
|
|
|
signoffType="customer"
|
|
|
|
|
nameLabel="Customer name"
|
|
|
|
|
onAdded={vi.fn()}
|
|
|
|
|
{...overrides}
|
|
|
|
|
/>,
|
refactor: move vendor-portal dispatch actions and 2 pages onto proper data hooks
Two groups of components were calling the API directly instead of using
this codebase's standard React Query hooks, with no shared caching and
duplicated submit/error-handling boilerplate.
- checklist-section.tsx, comments-section.tsx, signoff-section.tsx,
uplift-requests-section.tsx: replace direct vendorPortalApi calls and
hand-rolled submitting/error state with mutation hooks in the new
use-vendor-portal-dispatch-actions.ts. uplift-requests-section keeps
its existing multi-step evidence-upload orchestration intact and only
swaps the 3 raw API calls for mutateAsync, since restructuring that
flow around declarative mutation callbacks carried more risk than
value. Caught and fixed a real bug along the way: two components were
passing a prop callback directly as a mutation's onSuccess, which
would have forwarded React Query's extra callback arguments into it.
- followups/index.tsx, vendors/preferences.tsx: split from a single
file doing state, API calls, and rendering into a hook (owns data
and handlers) plus small presentational components, matching the
task-templates refactor pattern. Added a missing centralized query
key (vendorOperations.sitePreferences) that preferences.tsx was
building as a raw inline array.
- use-list-page-filters.ts: small shared hook for search/page/pageSize
state, used by the new followups hook.
29 new/updated tests across all touched files; none of these had
coverage for their mutation/error paths before.
2026-09-25 10:53:23 -05:00
|
|
|
{ withAuth: false },
|
2026-08-10 18:25:57 -03:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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();
|
|
|
|
|
});
|
refactor: move vendor-portal dispatch actions and 2 pages onto proper data hooks
Two groups of components were calling the API directly instead of using
this codebase's standard React Query hooks, with no shared caching and
duplicated submit/error-handling boilerplate.
- checklist-section.tsx, comments-section.tsx, signoff-section.tsx,
uplift-requests-section.tsx: replace direct vendorPortalApi calls and
hand-rolled submitting/error state with mutation hooks in the new
use-vendor-portal-dispatch-actions.ts. uplift-requests-section keeps
its existing multi-step evidence-upload orchestration intact and only
swaps the 3 raw API calls for mutateAsync, since restructuring that
flow around declarative mutation callbacks carried more risk than
value. Caught and fixed a real bug along the way: two components were
passing a prop callback directly as a mutation's onSuccess, which
would have forwarded React Query's extra callback arguments into it.
- followups/index.tsx, vendors/preferences.tsx: split from a single
file doing state, API calls, and rendering into a hook (owns data
and handlers) plus small presentational components, matching the
task-templates refactor pattern. Added a missing centralized query
key (vendorOperations.sitePreferences) that preferences.tsx was
building as a raw inline array.
- use-list-page-filters.ts: small shared hook for search/page/pageSize
state, used by the new followups hook.
29 new/updated tests across all touched files; none of these had
coverage for their mutation/error paths before.
2026-09-25 10:53:23 -05:00
|
|
|
|
|
|
|
|
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 });
|
|
|
|
|
|
|
|
|
|
fireEvent.click(screen.getByRole("radio", { name: "Typed" }));
|
|
|
|
|
const [nameInput, signatureInput] = screen.getAllByRole("textbox");
|
|
|
|
|
fireEvent.change(nameInput, { target: { value: "Jane Doe" } });
|
|
|
|
|
fireEvent.change(signatureInput, { target: { value: "Jane Doe" } });
|
|
|
|
|
fireEvent.click(screen.getByRole("button", { name: "Submit Signoff" }));
|
|
|
|
|
|
|
|
|
|
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();
|
|
|
|
|
|
|
|
|
|
fireEvent.click(screen.getByRole("radio", { name: "Typed" }));
|
|
|
|
|
const [nameInput, signatureInput] = screen.getAllByRole("textbox");
|
|
|
|
|
fireEvent.change(nameInput, { target: { value: "Jane Doe" } });
|
|
|
|
|
fireEvent.change(signatureInput, { target: { value: "Jane Doe" } });
|
|
|
|
|
fireEvent.click(screen.getByRole("button", { name: "Submit Signoff" }));
|
|
|
|
|
|
|
|
|
|
expect(
|
|
|
|
|
await screen.findByText("Unable to submit the signoff. Please try again."),
|
|
|
|
|
).toBeInTheDocument();
|
|
|
|
|
});
|
2026-08-10 18:25:57 -03:00
|
|
|
});
|