import { fireEvent, render, screen, waitFor, within } from "@testing-library/react"; import { afterEach, beforeAll, describe, expect, it, vi } from "vitest"; import { CustomerForm } from "./customer-form"; const { createMock, findCustomerDuplicatesMock, getAllCompaniesMock, } = vi.hoisted(() => ({ createMock: vi.fn(), findCustomerDuplicatesMock: vi.fn(), getAllCompaniesMock: vi.fn(), })); vi.mock("@/lib/pocketbase", () => ({ __esModule: true, default: () => ({ collection: () => ({ create: createMock, update: vi.fn(), }), }), })); vi.mock("sonner", () => ({ toast: { error: vi.fn(), success: vi.fn() }, })); vi.mock("@/lib/pb-helpers", () => ({ getAllCompanies: getAllCompaniesMock, findCustomerDuplicates: findCustomerDuplicatesMock, })); vi.mock("@/components/ui/company-search", () => ({ CompanySearch: () =>
Company Search
, })); vi.mock("@/components/ui/user-search", () => ({ UserSearch: () =>
User Search
, })); afterEach(() => { vi.clearAllMocks(); getAllCompaniesMock.mockResolvedValue([]); findCustomerDuplicatesMock.mockResolvedValue([]); }); // The form's useEffect on mount calls `getAllCompanies()` to populate // the company combobox. The pre-existing test (delivery-coords // position) never awaited that promise — it just used `render` — and // the form reads `companies.length` in a separate effect. Provide a // safe default before each test. beforeAll(() => { getAllCompaniesMock.mockResolvedValue([]); }); describe("CustomerForm", () => { it("shows delivery coordinates in the contact section before details", () => { render( undefined} onSaved={() => undefined} /> ); const content = document.body.textContent ?? ""; expect(content.indexOf("Delivery Coordinates")).toBeGreaterThan(content.indexOf("Contact")); expect(content.indexOf("Delivery Coordinates")).toBeLessThan(content.indexOf("Details")); }); it("blocks save when an existing customer has the same (name, company) pair and lets the operator override", async () => { // Form is filling out (John Smith, company=acme-id, email=). // PB returns an existing customer with the same name+company — // a real collision. Submit should pop the duplicate dialog. findCustomerDuplicatesMock.mockResolvedValueOnce([ { id: "cu-existing", customer: "John Smith", company: "acme-id", email: "", coordinates: { lon: 0, lat: 0 }, } as never, ]); createMock.mockResolvedValue({ id: "cu1" }); render( undefined} onSaved={() => undefined} /> ); const form = screen.getByRole("dialog").querySelector("form")!; fireEvent.change(within(form).getByPlaceholderText("Full name or business name"), { target: { value: "john smith" }, }); fireEvent.click(within(form).getByRole("button", { name: "Create Customer" })); // Wait for the dialog to show the existing match. await waitFor(() => { expect(screen.getByText("John Smith")).toBeInTheDocument(); }); // The write was NOT performed yet. expect(createMock).not.toHaveBeenCalled(); // "Save anyway" should land the write. fireEvent.click(screen.getByRole("button", { name: /save anyway/i })); await waitFor(() => { expect(createMock).toHaveBeenCalledWith( expect.objectContaining({ customer: "john smith" }) ); }); }); it("passes the current record's id as excludeId in edit mode", async () => { // The form is bound to an existing customer. The duplicate check // must exclude the record being edited so it doesn't flag itself. const existing = { id: "cu-self", customer: "John Smith" }; findCustomerDuplicatesMock.mockResolvedValue([]); render( undefined} onSaved={() => undefined} customer={existing as never} /> ); const form = screen.getByRole("dialog").querySelector("form")!; fireEvent.change(within(form).getByPlaceholderText("Full name or business name"), { target: { value: "John Smith" }, }); // The live check debounces by 300ms — wait for the helper to be // called with the record's id as the excludeId. await waitFor(() => { expect(findCustomerDuplicatesMock).toHaveBeenCalledWith( "John Smith", "", "", "cu-self" ); }); }); });