import { act, renderHook, waitFor } from "@testing-library/react"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import type { ReactNode } from "react"; import { useController } from "react-hook-form"; import { beforeEach, describe, expect, it, vi } from "vitest"; const rosterGet = vi.fn(); const saveMutate = vi.fn(); vi.mock("@/domain/vendors/api/vendor-company-roster-api", () => ({ vendorCompanyRosterApi: { get: (...args: unknown[]) => rosterGet(...args) }, })); vi.mock("@/domain/vendors/use-cases/use-vendor-company-roster", () => ({ useVendorCompanyRoster: () => ({ data: undefined, isLoading: false, isError: false, error: null, refetch: vi.fn(), }), })); vi.mock("@/domain/vendors/use-cases/use-vendor-facets", () => ({ useVendorFacets: () => ({ data: { companies: [], trades: [] } }), })); vi.mock("@/domain/vendors/use-cases/use-save-vendor-company-roster", async () => { const actual = await vi.importActual< typeof import("@/domain/vendors/use-cases/use-save-vendor-company-roster") >("@/domain/vendors/use-cases/use-save-vendor-company-roster"); return { ...actual, useSaveVendorCompanyRoster: () => ({ mutate: saveMutate, isPending: false }), }; }); import { useVendorRosterForm } from "@/app/(protected)/vendors/_components/use-vendor-roster-form"; import type { VendorCompanyRoster, VendorFacetCompany } from "@/domain/vendors/types/vendor"; const company: VendorFacetCompany = { companyId: 5, name: "Gateway Plumbing", companyPhone: "(314) 555-0100", email: "dispatch@gateway.test", address: "1 Main St", city: "St. Louis", state: "MO", zip: "63101", googleMapsUrl: "", }; const roster: VendorCompanyRoster = { companyId: 5, rowVersion: "rv-1", name: "Gateway Plumbing", companyPhone: "(314) 555-0100", email: "dispatch@gateway.test", address: "1 Main St", city: "St. Louis", state: "MO", zip: "63101", googleMapsUrl: "", notes: "", technicians: [], }; const secondCompany: VendorFacetCompany = { ...company, companyId: 6, name: "Metro HVAC", companyPhone: "(314) 555-0110", email: "dispatch@metro.test", }; const secondRoster: VendorCompanyRoster = { ...roster, companyId: 6, name: "Metro HVAC", companyPhone: "(314) 555-0110", email: "dispatch@metro.test", }; function createClient(): QueryClient { return new QueryClient({ defaultOptions: { queries: { retry: false } } }); } function makeWrapper(client: QueryClient) { return function Wrapper({ children }: { children: ReactNode }) { return {children}; }; } function deferRoster(): { promise: Promise; resolve: (value: VendorCompanyRoster) => void; } { let resolve!: (value: VendorCompanyRoster) => void; const promise = new Promise((resolver) => { resolve = resolver; }); return { promise, resolve }; } describe("useVendorRosterForm load-error handling", () => { beforeEach(() => { rosterGet.mockReset(); saveMutate.mockReset(); }); it("shows a safe retryable error when selecting an existing company fetch is rejected", async () => { rosterGet.mockRejectedValueOnce(new Error("Internal server error")); const { result } = renderHook(() => useVendorRosterForm({ mode: "create" }), { wrapper: makeWrapper(createClient()), }); expect(result.current.loadErrorMessage).toBeNull(); await act(async () => { await result.current.selectCompany(company); }); expect(result.current.loadErrorMessage).toBe("Couldn't load that company's roster. Try again."); expect(result.current.selectedCompanyId).toBeNull(); }); it("preserves entered form data when selecting an existing company fetch is rejected", async () => { rosterGet.mockRejectedValueOnce(new Error("Internal server error")); const { result } = renderHook( () => { const form = useVendorRosterForm({ mode: "create" }); const name = useController({ control: form.control, name: "name" }); return { form, name }; }, { wrapper: makeWrapper(createClient()) }, ); await act(async () => { result.current.name.field.onChange("Draft Vendor"); }); expect(result.current.name.field.value).toBe("Draft Vendor"); await act(async () => { await result.current.form.selectCompany(company); }); expect(result.current.name.field.value).toBe("Draft Vendor"); expect(result.current.form.loadErrorMessage).toBe( "Couldn't load that company's roster. Try again.", ); }); it("recovers and populates the form on retry after a rejected fetch", async () => { rosterGet.mockRejectedValueOnce(new Error("boom")); const { result } = renderHook(() => useVendorRosterForm({ mode: "create" }), { wrapper: makeWrapper(createClient()), }); await act(async () => { await result.current.selectCompany(company); }); expect(result.current.loadErrorMessage).not.toBeNull(); rosterGet.mockResolvedValueOnce(roster); await act(async () => { result.current.retryLoad(); }); await waitFor(() => expect(result.current.loadErrorMessage).toBeNull()); expect(result.current.selectedCompanyId).toBe(5); }); it("clears the selected roster when the form is reset", async () => { rosterGet.mockResolvedValueOnce(roster); const { result } = renderHook(() => useVendorRosterForm({ mode: "create" }), { wrapper: makeWrapper(createClient()), }); await act(async () => { await result.current.selectCompany(company); }); expect(result.current.selectedCompanyId).toBe(5); act(() => result.current.resetForm()); expect(result.current.selectedCompanyId).toBeNull(); }); it("does not expose raw server details in the error message", async () => { rosterGet.mockRejectedValueOnce(new Error("Database connection failed at 10.0.0.1")); const { result } = renderHook(() => useVendorRosterForm({ mode: "create" }), { wrapper: makeWrapper(createClient()), }); await act(async () => { await result.current.selectCompany(company); }); expect(result.current.loadErrorMessage).not.toContain("Database"); expect(result.current.loadErrorMessage).not.toContain("10.0.0.1"); }); }); describe("useVendorRosterForm prototype defaults", () => { it("does not persist the prototype's untouched optional technician card", () => { const { result } = renderHook( () => useVendorRosterForm({ mode: "create", startWithTechnician: true }), { wrapper: makeWrapper(createClient()) }, ); act(() => { result.current.submit({ name: "Independent Vendor LLC", companyPhone: "(314) 555-0199", email: "", address: "", city: "", state: "", zip: "", googleMapsUrl: "", notes: "", technicians: [ { contactName: "", phone: "", email: "", preferredContact: "Phone", tradeSpecialties: "", isActive: true, }, ], }); }); expect(saveMutate).toHaveBeenCalledWith( expect.objectContaining({ values: expect.objectContaining({ technicians: [] }) }), expect.any(Object), ); }); }); describe("useVendorRosterForm stale-selection handling", () => { beforeEach(() => { rosterGet.mockReset(); }); it("keeps the newest company roster when two lookups resolve out of order", async () => { const first = deferRoster(); const second = deferRoster(); rosterGet.mockImplementationOnce(() => first.promise); rosterGet.mockImplementationOnce(() => second.promise); const { result } = renderHook( () => { const form = useVendorRosterForm({ mode: "create" }); const name = useController({ control: form.control, name: "name" }); return { form, name }; }, { wrapper: makeWrapper(createClient()) }, ); let firstSelect!: Promise; let secondSelect!: Promise; act(() => { firstSelect = result.current.form.selectCompany(company); }); act(() => { secondSelect = result.current.form.selectCompany(secondCompany); }); await act(async () => { second.resolve(secondRoster); await secondSelect; }); await act(async () => { first.resolve(roster); await firstSelect; }); expect(result.current.form.selectedCompanyId).toBe(6); expect(result.current.name.field.value).toBe("Metro HVAC"); }); it("keeps a free-text draft when a pending lookup resolves after clearing to free text", async () => { const lookup = deferRoster(); rosterGet.mockImplementationOnce(() => lookup.promise); const { result } = renderHook( () => { const form = useVendorRosterForm({ mode: "create" }); const name = useController({ control: form.control, name: "name" }); return { form, name }; }, { wrapper: makeWrapper(createClient()) }, ); let pendingSelect!: Promise; act(() => { pendingSelect = result.current.form.selectCompany(company); }); act(() => { result.current.form.clearSelectedCompany("Draft Vendor"); }); await act(async () => { result.current.name.field.onChange("Draft Vendor"); }); expect(result.current.name.field.value).toBe("Draft Vendor"); await act(async () => { lookup.resolve(roster); await pendingSelect; }); expect(result.current.form.selectedCompanyId).toBeNull(); expect(result.current.name.field.value).toBe("Draft Vendor"); }); });