import { act, fireEvent, render, renderHook, screen } from "@testing-library/react"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { WizardStepVendorTime } from "@/app/(protected)/workorders/_components/wizard/wizard-step-vendor-time"; import { useWizardCreateWithTechnicians } from "@/app/(protected)/workorders/_components/wizard/use-wizard-create-with-technicians"; import type { VendorDropdownItem } from "@/domain/work-orders/types/work-order"; import { EMPTY_WIZARD_DRAFT, type WorkOrderWizardDraft, } from "@/domain/work-orders/types/work-order-wizard"; const mutateAsync = vi.fn(); vi.mock("@/domain/vendors/use-cases/use-add-technicians-to-company", () => ({ useAddTechniciansToCompany: () => ({ mutateAsync, isPending: false }), })); vi.mock("react-toastify", () => ({ toast: { error: vi.fn(), success: vi.fn() } })); const vendors: VendorDropdownItem[] = [ { id: 41, companyName: "Vinewood LLC", contactName: "Adam Whyte", tradeSpecialties: "HVAC", address: "1 Market St", distanceMiles: null, }, { id: 52, companyName: "Gateway Plumbing", contactName: "Casey Pipe", tradeSpecialties: "Plumbing", address: "", distanceMiles: null, }, ]; function draftWith(patch: Partial): WorkOrderWizardDraft { return { ...EMPTY_WIZARD_DRAFT, type: "PM", status: "Incomplete", ...patch }; } describe("WizardStepVendorTime inline technician (SH-364)", () => { it("hides the add action until a company is picked", () => { render( , ); expect(screen.queryByRole("button", { name: "+ Add technician" })).not.toBeInTheDocument(); expect(screen.getByRole("button", { name: /pick a company first/i })).toBeDisabled(); }); it("stages a new technician row for the picked company", () => { const onPatch = vi.fn(); render( , ); fireEvent.click(screen.getByRole("button", { name: "+ Add technician" })); expect(onPatch).toHaveBeenCalledWith({ newTechnicians: [{ name: "", phone: "" }] }); }); it("scopes technicians to the picked company", () => { render( , ); expect(screen.getByRole("group", { name: "New Technician 1" })).toBeInTheDocument(); fireEvent.click(screen.getByRole("button", { name: /select technician/i })); expect(screen.getByRole("button", { name: "Adam Whyte" })).toBeInTheDocument(); expect(screen.queryByRole("button", { name: "Casey Pipe" })).not.toBeInTheDocument(); }); it("clears the picked vendor and staged technicians when the company changes", () => { const onPatch = vi.fn(); render( , ); fireEvent.click(screen.getByRole("button", { name: "Vinewood LLC" })); fireEvent.click(screen.getByRole("button", { name: "Gateway Plumbing" })); expect(onPatch).toHaveBeenCalledWith({ vendorCompany: "Gateway Plumbing", vendorId: "", vendorName: "", techPhone: "", vendorNotes: "", newTechnicians: [], }); }); it("preserves the picked vendor and staged technicians when the same company is re-picked", () => { const onPatch = vi.fn(); render( , ); fireEvent.click(screen.getByRole("button", { name: "Vinewood LLC" })); const companyOptions = screen.getAllByRole("button", { name: "Vinewood LLC" }); fireEvent.click(companyOptions[companyOptions.length - 1]); expect(onPatch).toHaveBeenCalledWith({ vendorCompany: "Vinewood LLC" }); }); }); describe("useWizardCreateWithTechnicians (SH-364)", () => { beforeEach(() => { mutateAsync.mockReset(); }); it("creates the work order directly when no technician was staged", () => { const createMutation = { mutate: vi.fn(), isPending: false }; const { result } = renderHook(() => useWizardCreateWithTechnicians({ vendors, patchDraft: vi.fn(), createMutation }), ); const draft = draftWith({ vendorCompany: "Vinewood LLC", newTechnicians: [] }); result.current.mutate(draft); expect(mutateAsync).not.toHaveBeenCalled(); expect(createMutation.mutate).toHaveBeenCalledWith(draft); }); it("persists staged technicians first and assigns a single new technician", async () => { mutateAsync.mockResolvedValue([{ ...vendors[0], id: 99, contactName: "Jordan Lee" }]); const createMutation = { mutate: vi.fn(), isPending: false }; const patchDraft = vi.fn(); const { result } = renderHook(() => useWizardCreateWithTechnicians({ vendors, patchDraft, createMutation }), ); await act(async () => { result.current.mutate( draftWith({ vendorCompany: "Vinewood LLC", newTechnicians: [ { name: "Jordan Lee", phone: "(314) 555-0199" }, { name: " ", phone: "" }, ], }), ); }); expect(mutateAsync).toHaveBeenCalledWith({ anchorVendorId: 41, technicians: [ { contactName: "Jordan Lee", phone: "(314) 555-0199", tradeSpecialties: "HVAC" }, ], }); const expectedPatch = { newTechnicians: [], vendorId: "99", vendorName: "Vinewood LLC", techPhone: "(314) 555-0199", }; expect(patchDraft).toHaveBeenCalledWith(expectedPatch); expect(createMutation.mutate).toHaveBeenCalledWith(expect.objectContaining(expectedPatch)); }); it("blocks Create and skips the roster save when a staged phone is not a valid number", async () => { const createMutation = { mutate: vi.fn(), isPending: false }; const { result } = renderHook(() => useWizardCreateWithTechnicians({ vendors, patchDraft: vi.fn(), createMutation }), ); await act(async () => { result.current.mutate( draftWith({ vendorCompany: "Vinewood LLC", newTechnicians: [{ name: "Jordan Lee", phone: "(314) 555-01" }], }), ); }); expect(mutateAsync).not.toHaveBeenCalled(); expect(createMutation.mutate).not.toHaveBeenCalled(); }); it("allows a staged technician with a blank phone, matching the modal path", async () => { mutateAsync.mockResolvedValue([{ ...vendors[0], id: 99, contactName: "Jordan Lee" }]); const createMutation = { mutate: vi.fn(), isPending: false }; const { result } = renderHook(() => useWizardCreateWithTechnicians({ vendors, patchDraft: vi.fn(), createMutation }), ); await act(async () => { result.current.mutate( draftWith({ vendorCompany: "Vinewood LLC", newTechnicians: [{ name: "Jordan Lee", phone: "" }], }), ); }); expect(mutateAsync).toHaveBeenCalledWith({ anchorVendorId: 41, technicians: [{ contactName: "Jordan Lee", phone: "", tradeSpecialties: "HVAC" }], }); expect(createMutation.mutate).toHaveBeenCalled(); }); it("does not create the work order when the technician save fails", async () => { mutateAsync.mockRejectedValue(new Error("boom")); const createMutation = { mutate: vi.fn(), isPending: false }; const { result } = renderHook(() => useWizardCreateWithTechnicians({ vendors, patchDraft: vi.fn(), createMutation }), ); await act(async () => { result.current.mutate( draftWith({ vendorCompany: "Vinewood LLC", newTechnicians: [{ name: "Jordan Lee", phone: "" }], }), ); }); expect(createMutation.mutate).not.toHaveBeenCalled(); }); });