mirror of
https://github.com/Sea-Haven-Industries/shoc-frontend-new.git
synced 2026-10-07 16:19:01 +00:00
318 lines
9.5 KiB
TypeScript
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");
|
|
});
|
|
});
|