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"
);
});
});
});