import { cleanup, fireEvent, render, screen, waitFor, within } from "@testing-library/react"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { SupplierForm } from "./supplier-form"; /** * Tests for the SupplierForm's duplicate-detection flow. There was no * prior supplier-form test file, so this also covers the basic save * path that the company/customer tests already exercise — we want * the same shape of safety net for every entity that grew the dup * check. */ const { createMock, findSupplierDuplicatesMock } = vi.hoisted(() => ({ createMock: vi.fn(), findSupplierDuplicatesMock: 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", () => ({ findSupplierDuplicates: findSupplierDuplicatesMock, })); beforeEach(() => { // Default for every debounced duplicate check. Tests layer // mockResolvedValueOnce on top; later keystrokes fall back to []. findSupplierDuplicatesMock.mockResolvedValue([]); }); afterEach(() => { // Unmount first so the 300ms duplicate-check debounce is cleared // before the mocks are reset (otherwise it fires into a bare mock). cleanup(); vi.clearAllMocks(); }); describe("SupplierForm", () => { it("blocks save when an existing supplier has the same name and lets the operator override", async () => { // Form is filling out "West Rand". PB returns an existing row // with the same name (case-insensitive). Submit should pop the // duplicate dialog instead of writing. findSupplierDuplicatesMock.mockResolvedValueOnce([ { id: "sup-existing", suppliername: "West Rand" } as never, ]); createMock.mockResolvedValue({ id: "sup1" }); render( undefined} onSaved={() => undefined} /> ); const form = screen.getByRole("dialog").querySelector("form")!; fireEvent.change(within(form).getByPlaceholderText("e.g. West Rand Industrial"), { target: { value: "west rand" }, }); fireEvent.click(within(form).getByRole("button", { name: "Create Supplier" })); // Wait for the dialog to show the existing match. await waitFor(() => { expect(screen.getByText("West Rand")).toBeInTheDocument(); }); // The write was NOT performed yet. expect(createMock).not.toHaveBeenCalled(); // "Save anyway" should land the write with the typed value. fireEvent.click(screen.getByRole("button", { name: /save anyway/i })); await waitFor(() => { expect(createMock).toHaveBeenCalledWith( expect.objectContaining({ suppliername: "west rand" }) ); }); }); it("excludes the current record from the duplicate check in edit mode", async () => { // Bound to an existing supplier; typing the same name shouldn't // flag itself. We assert the helper was called with the record's // id as the excludeId. const existing = { id: "sup-self", suppliername: "West Rand" }; findSupplierDuplicatesMock.mockResolvedValue([]); render( undefined} onSaved={() => undefined} supplier={existing as never} /> ); const form = screen.getByRole("dialog").querySelector("form")!; fireEvent.change(within(form).getByPlaceholderText("e.g. West Rand Industrial"), { target: { value: "West Rand" }, }); // Live check debounces by 300ms — wait for the helper call. await waitFor(() => { expect(findSupplierDuplicatesMock).toHaveBeenCalledWith( "West Rand", "sup-self" ); }); }); });