mirror of
https://github.com/Sea-Haven-Industries/shoc-frontend-new.git
synced 2026-10-07 16:19:01 +00:00
* fix(vendors): clarify blank technician draft state * test(vendors): update technician section snapshots --------- Co-authored-by: Codex Review Integration <codex-review@local.invalid>
716 lines
22 KiB
TypeScript
716 lines
22 KiB
TypeScript
import { act, renderHook, waitFor } from "@testing-library/react";
|
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
|
import type { ReactNode } from "react";
|
|
import { useController, useFieldArray } from "react-hook-form";
|
|
import { toast } from "react-toastify";
|
|
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: () => rosterQueryResult,
|
|
}));
|
|
|
|
vi.mock("react-toastify", () => ({ toast: { success: vi.fn(), error: vi.fn() } }));
|
|
|
|
let rosterQueryResult: {
|
|
data: VendorCompanyRoster | undefined;
|
|
isLoading: boolean;
|
|
isError: boolean;
|
|
error: Error | null;
|
|
refetch: ReturnType<typeof vi.fn>;
|
|
} = { 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 { VendorCompanyRosterFormValues } from "@/domain/vendors/schemas/vendor-roster-schema";
|
|
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",
|
|
};
|
|
|
|
const rosterWithTechnicians: VendorCompanyRoster = {
|
|
...roster,
|
|
technicians: [
|
|
{
|
|
id: 11,
|
|
contactName: "Ray Holt",
|
|
phone: "(314) 555-0122",
|
|
email: "",
|
|
preferredContact: "Phone",
|
|
tradeSpecialties: "Plumbing",
|
|
isActive: true,
|
|
totalJobs: 0,
|
|
},
|
|
{
|
|
id: 12,
|
|
contactName: "Amy Santiago",
|
|
phone: "(314) 555-0133",
|
|
email: "",
|
|
preferredContact: "Text",
|
|
tradeSpecialties: "HVAC",
|
|
isActive: true,
|
|
totalJobs: 0,
|
|
},
|
|
],
|
|
};
|
|
|
|
const newTechnician = {
|
|
contactName: "Dana Kim",
|
|
phone: "(314) 555-0144",
|
|
email: "dana@test.test",
|
|
preferredContact: "Text" as const,
|
|
tradeSpecialties: "HVAC",
|
|
isActive: true,
|
|
};
|
|
|
|
function submitValues(
|
|
technicians: VendorCompanyRosterFormValues["technicians"],
|
|
): VendorCompanyRosterFormValues {
|
|
return {
|
|
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,
|
|
};
|
|
}
|
|
|
|
function resetRosterQuery(): void {
|
|
rosterQueryResult = {
|
|
data: undefined,
|
|
isLoading: false,
|
|
isError: false,
|
|
error: null,
|
|
refetch: vi.fn(),
|
|
};
|
|
}
|
|
|
|
beforeEach(() => {
|
|
resetRosterQuery();
|
|
});
|
|
|
|
function createClient(): QueryClient {
|
|
return new QueryClient({ defaultOptions: { queries: { retry: false } } });
|
|
}
|
|
|
|
/**
|
|
* SH-250: mirrors the app's real staleTime (src/lib/query/query-client.ts). The default
|
|
* test client uses staleTime 0, which silently masks cache-staleness defects on the
|
|
* reload path — the conflict recovery must not depend on an empty cache.
|
|
*/
|
|
function createCachingClient(): QueryClient {
|
|
return new QueryClient({
|
|
defaultOptions: { queries: { retry: false, staleTime: 5 * 60 * 1000 } },
|
|
});
|
|
}
|
|
|
|
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 technician removal", () => {
|
|
it("keeps a valid roster saveable after an existing technician is removed", async () => {
|
|
rosterQueryResult = {
|
|
data: rosterWithTechnicians,
|
|
isLoading: false,
|
|
isError: false,
|
|
error: null,
|
|
refetch: vi.fn(),
|
|
};
|
|
const { result } = renderHook(
|
|
() => {
|
|
const form = useVendorRosterForm({ mode: "update", companyId: 5 });
|
|
const technicians = useFieldArray({ control: form.control, name: "technicians" });
|
|
return { form, technicians };
|
|
},
|
|
{ wrapper: makeWrapper(createClient()) },
|
|
);
|
|
|
|
await waitFor(() => expect(result.current.form.isValid).toBe(true));
|
|
act(() => result.current.technicians.remove(1));
|
|
|
|
await waitFor(() => expect(result.current.form.isValid).toBe(true));
|
|
});
|
|
});
|
|
|
|
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");
|
|
});
|
|
});
|
|
|
|
describe("useVendorRosterForm additive add flow (SH-250)", () => {
|
|
beforeEach(() => {
|
|
rosterGet.mockReset();
|
|
saveMutate.mockReset();
|
|
vi.mocked(toast.error).mockReset();
|
|
});
|
|
|
|
it("issues an additive add — never update — when an existing company is selected", async () => {
|
|
rosterGet.mockResolvedValueOnce(roster);
|
|
const { result } = renderHook(
|
|
() => {
|
|
const form = useVendorRosterForm({ mode: "create", startWithTechnician: true });
|
|
const technician = useController({
|
|
control: form.control,
|
|
name: "technicians.0.contactName",
|
|
});
|
|
return { form, technician };
|
|
},
|
|
{ wrapper: makeWrapper(createClient()) },
|
|
);
|
|
|
|
await act(async () => {
|
|
await result.current.form.selectCompany(company);
|
|
});
|
|
await act(async () => {
|
|
result.current.technician.field.onChange("Dana Kim");
|
|
});
|
|
act(() => {
|
|
result.current.form.submit(submitValues([newTechnician]));
|
|
});
|
|
|
|
expect(saveMutate).toHaveBeenCalledTimes(1);
|
|
const input = saveMutate.mock.calls[0]?.[0] as { mode: string };
|
|
expect(input.mode).toBe("add");
|
|
expect(saveMutate).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
companyId: 5,
|
|
rowVersion: "rv-1",
|
|
originalRoster: roster,
|
|
values: expect.objectContaining({ technicians: [newTechnician] }),
|
|
}),
|
|
expect.any(Object),
|
|
);
|
|
});
|
|
|
|
it("still creates via POST when no existing company is matched", () => {
|
|
const { result } = renderHook(
|
|
() => useVendorRosterForm({ mode: "create", startWithTechnician: true }),
|
|
{ wrapper: makeWrapper(createClient()) },
|
|
);
|
|
|
|
act(() => {
|
|
result.current.submit(submitValues([newTechnician]));
|
|
});
|
|
|
|
expect(saveMutate).toHaveBeenCalledTimes(1);
|
|
expect(saveMutate.mock.calls[0]?.[0]).toEqual(
|
|
expect.objectContaining({ mode: "create", companyId: undefined, rowVersion: undefined }),
|
|
);
|
|
});
|
|
|
|
it("blocks submit when the selected company has nothing to add", async () => {
|
|
rosterGet.mockResolvedValueOnce(roster);
|
|
const { result } = renderHook(() => useVendorRosterForm({ mode: "create" }), {
|
|
wrapper: makeWrapper(createClient()),
|
|
});
|
|
|
|
await act(async () => {
|
|
await result.current.selectCompany(company);
|
|
});
|
|
act(() => {
|
|
result.current.submit(submitValues([]));
|
|
});
|
|
|
|
expect(saveMutate).not.toHaveBeenCalled();
|
|
expect(vi.mocked(toast.error)).toHaveBeenCalledTimes(1);
|
|
expect(vi.mocked(toast.error)).toHaveBeenCalledWith(
|
|
"No changes to save. Enter technician details or update a company field.",
|
|
);
|
|
});
|
|
|
|
it("keeps the entered technician and retries with the fresh rowVersion after reload", async () => {
|
|
rosterGet.mockResolvedValueOnce(roster);
|
|
const { result } = renderHook(
|
|
() => {
|
|
const form = useVendorRosterForm({ mode: "create", startWithTechnician: true });
|
|
const technician = useController({
|
|
control: form.control,
|
|
name: "technicians.0.contactName",
|
|
});
|
|
return { form, technician };
|
|
},
|
|
{ wrapper: makeWrapper(createClient()) },
|
|
);
|
|
|
|
await act(async () => {
|
|
await result.current.form.selectCompany(company);
|
|
});
|
|
await act(async () => {
|
|
result.current.technician.field.onChange("Dana Kim");
|
|
});
|
|
act(() => {
|
|
result.current.form.submit(submitValues([newTechnician]));
|
|
});
|
|
expect(saveMutate.mock.calls[0]?.[0]).toEqual(expect.objectContaining({ rowVersion: "rv-1" }));
|
|
|
|
rosterGet.mockResolvedValueOnce({ ...roster, rowVersion: "rv-2" });
|
|
await act(async () => {
|
|
result.current.form.reload();
|
|
});
|
|
await waitFor(() => expect(result.current.form.selectedCompanyId).toBe(5));
|
|
|
|
act(() => {
|
|
result.current.form.submit(submitValues([newTechnician]));
|
|
});
|
|
expect(saveMutate.mock.calls[1]?.[0]).toEqual(
|
|
expect.objectContaining({ mode: "add", rowVersion: "rv-2" }),
|
|
);
|
|
});
|
|
|
|
it("refreshes the visible company fields on reload, not just rowVersion", async () => {
|
|
rosterGet.mockResolvedValueOnce(roster);
|
|
const { result } = renderHook(
|
|
() => {
|
|
const form = useVendorRosterForm({ mode: "create", startWithTechnician: true });
|
|
const name = useController({ control: form.control, name: "name" });
|
|
return { form, name };
|
|
},
|
|
{ wrapper: makeWrapper(createClient()) },
|
|
);
|
|
|
|
await act(async () => {
|
|
await result.current.form.selectCompany(company);
|
|
});
|
|
expect(result.current.name.field.value).toBe("Gateway Plumbing");
|
|
|
|
// Someone else renamed the company, which is what caused the 409.
|
|
rosterGet.mockResolvedValueOnce({
|
|
...roster,
|
|
rowVersion: "rv-2",
|
|
name: "Gateway Plumbing & Drain",
|
|
});
|
|
await act(async () => {
|
|
result.current.form.reload();
|
|
});
|
|
|
|
// Reload must not leave the form on the pre-conflict name, or the retry
|
|
// would diff stale fields and overwrite the concurrent update.
|
|
await waitFor(() => expect(result.current.name.field.value).toBe("Gateway Plumbing & Drain"));
|
|
});
|
|
|
|
it("reload bypasses the cache so the retry carries the fresh rowVersion", async () => {
|
|
rosterGet.mockResolvedValueOnce(roster);
|
|
const { result } = renderHook(
|
|
() => useVendorRosterForm({ mode: "create", startWithTechnician: true }),
|
|
{ wrapper: makeWrapper(createCachingClient()) },
|
|
);
|
|
|
|
await act(async () => {
|
|
await result.current.selectCompany(company);
|
|
});
|
|
expect(rosterGet).toHaveBeenCalledTimes(1);
|
|
|
|
act(() => {
|
|
result.current.submit(submitValues([newTechnician]));
|
|
});
|
|
expect(saveMutate.mock.calls[0]?.[0]).toEqual(expect.objectContaining({ rowVersion: "rv-1" }));
|
|
|
|
// The 409 never invalidates this key, and in the Add flow nothing else observes it.
|
|
// Without staleTime: 0 on the reload fetch, the cached rv-1 is served straight back
|
|
// and every retry 409s again.
|
|
rosterGet.mockResolvedValueOnce({ ...roster, rowVersion: "rv-2" });
|
|
await act(async () => {
|
|
result.current.reload();
|
|
});
|
|
|
|
await waitFor(() => expect(rosterGet).toHaveBeenCalledTimes(2));
|
|
|
|
act(() => {
|
|
result.current.submit(submitValues([newTechnician]));
|
|
});
|
|
expect(saveMutate.mock.calls[1]?.[0]).toEqual(
|
|
expect.objectContaining({ mode: "add", rowVersion: "rv-2" }),
|
|
);
|
|
});
|
|
|
|
it("does not regress the Edit Vendor reconcile path (mode update)", () => {
|
|
rosterQueryResult = {
|
|
data: rosterWithTechnicians,
|
|
isLoading: false,
|
|
isError: false,
|
|
error: null,
|
|
refetch: vi.fn(),
|
|
};
|
|
const { result } = renderHook(() => useVendorRosterForm({ mode: "update", vendorId: 7 }), {
|
|
wrapper: makeWrapper(createClient()),
|
|
});
|
|
|
|
act(() => {
|
|
result.current.submit(submitValues([...rosterWithTechnicians.technicians, newTechnician]));
|
|
});
|
|
|
|
expect(saveMutate).toHaveBeenCalledTimes(1);
|
|
expect(saveMutate.mock.calls[0]?.[0]).toEqual(
|
|
expect.objectContaining({
|
|
mode: "update",
|
|
companyId: 5,
|
|
rowVersion: "rv-1",
|
|
originalRoster: rosterWithTechnicians,
|
|
values: expect.objectContaining({
|
|
technicians: [...rosterWithTechnicians.technicians, newTechnician],
|
|
}),
|
|
}),
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("useVendorRosterForm technician autopopulation (SH-246)", () => {
|
|
beforeEach(() => {
|
|
rosterGet.mockReset();
|
|
saveMutate.mockReset();
|
|
});
|
|
|
|
it("does not autopopulate technician fields when a company is selected", async () => {
|
|
rosterGet.mockResolvedValueOnce(rosterWithTechnicians);
|
|
const { result } = renderHook(
|
|
() => {
|
|
const form = useVendorRosterForm({ mode: "create" });
|
|
const technicians = useController({ control: form.control, name: "technicians" });
|
|
const name = useController({ control: form.control, name: "name" });
|
|
return { form, technicians, name };
|
|
},
|
|
{ wrapper: makeWrapper(createClient()) },
|
|
);
|
|
|
|
await act(async () => {
|
|
await result.current.form.selectCompany(company);
|
|
});
|
|
|
|
const technicians = result.current.technicians.field.value;
|
|
expect(technicians).toEqual([
|
|
{
|
|
contactName: "",
|
|
phone: "",
|
|
email: "",
|
|
tradeSpecialties: "",
|
|
isActive: true,
|
|
},
|
|
]);
|
|
// SH-250: the blank row must not carry a fabricated preferredContact, since
|
|
// the control was removed from the form and `append` omits it too.
|
|
expect(technicians[0]).not.toHaveProperty("preferredContact");
|
|
expect(technicians).not.toContainEqual(expect.objectContaining({ contactName: "Ray Holt" }));
|
|
expect(technicians).not.toContainEqual(
|
|
expect.objectContaining({ contactName: "Amy Santiago" }),
|
|
);
|
|
expect(result.current.name.field.value).toBe("Gateway Plumbing");
|
|
expect(result.current.form.selectedCompanyId).toBe(5);
|
|
});
|
|
|
|
it("keeps a typed technician when the company selection is cleared to free text", async () => {
|
|
rosterGet.mockResolvedValueOnce(roster);
|
|
const { result } = renderHook(
|
|
() => {
|
|
const form = useVendorRosterForm({ mode: "create", startWithTechnician: true });
|
|
const technician = useController({
|
|
control: form.control,
|
|
name: "technicians.0.contactName",
|
|
});
|
|
return { form, technician };
|
|
},
|
|
{ wrapper: makeWrapper(createClient()) },
|
|
);
|
|
|
|
await act(async () => {
|
|
await result.current.form.selectCompany(company);
|
|
});
|
|
await act(async () => {
|
|
result.current.technician.field.onChange("Dana Kim");
|
|
});
|
|
|
|
act(() => {
|
|
result.current.form.clearSelectedCompany("Draft Vendor");
|
|
});
|
|
|
|
expect(result.current.form.selectedCompanyId).toBeNull();
|
|
expect(result.current.technician.field.value).toBe("Dana Kim");
|
|
});
|
|
});
|