mirror of
https://github.com/Sea-Haven-Industries/shoc-frontend-new.git
synced 2026-10-07 16:19:01 +00:00
The reload fix re-seeded the form from queryClient.fetchQuery, which inherits the global 5-minute staleTime. A 409 never invalidates that key - useSaveVendorCompanyRoster invalidates only in onSuccess and its onError returns early for conflicts - and the Add flow has no mounted observer on it, so reload returned the cached roster with the same stale rowVersion and every retry 409'd again. Update mode was unaffected because it recovers via query.refetch(), which bypasses staleTime. Pass staleTime: 0 on that fetch, since its sole purpose is the freshest rowVersion. The regression test uses a client with the app's real staleTime; the default test client uses 0, which masked this entirely.
685 lines
21 KiB
TypeScript
685 lines
21 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: () => 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 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();
|
|
});
|
|
|
|
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();
|
|
});
|
|
|
|
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");
|
|
});
|
|
});
|