shoc-frontend-new/src/test/app/(protected)/vendors/use-vendor-roster-form.test.tsx
2026-08-06 22:23:30 -03:00

318 lines
9.5 KiB
TypeScript

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 <QueryClientProvider client={client}>{children}</QueryClientProvider>;
};
}
function deferRoster(): {
promise: Promise<VendorCompanyRoster>;
resolve: (value: VendorCompanyRoster) => void;
} {
let resolve!: (value: VendorCompanyRoster) => void;
const promise = new Promise<VendorCompanyRoster>((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<void>;
let secondSelect!: Promise<void>;
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<void>;
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");
});
});