This commit includes the temporary Next.js build cache files, public brand logos, and project configuration added in this change.
155 lines
5 KiB
TypeScript
155 lines
5 KiB
TypeScript
import { fireEvent, render, screen, waitFor, within } from "@testing-library/react";
|
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
|
|
import { CompanyForm } from "./company-form";
|
|
|
|
const { createMock, updateMock, toastErrorMock, toastSuccessMock, findCompanyDuplicatesMock } = vi.hoisted(() => ({
|
|
createMock: vi.fn(),
|
|
updateMock: vi.fn(),
|
|
toastErrorMock: vi.fn(),
|
|
toastSuccessMock: vi.fn(),
|
|
findCompanyDuplicatesMock: vi.fn(),
|
|
}));
|
|
|
|
vi.mock("@/lib/pocketbase", () => ({
|
|
__esModule: true,
|
|
default: () => ({
|
|
collection: () => ({
|
|
create: createMock,
|
|
update: updateMock,
|
|
}),
|
|
}),
|
|
}));
|
|
|
|
vi.mock("sonner", () => ({
|
|
toast: {
|
|
error: toastErrorMock,
|
|
success: toastSuccessMock,
|
|
},
|
|
}));
|
|
|
|
vi.mock("@/lib/pb-helpers", () => ({
|
|
findCompanyDuplicates: findCompanyDuplicatesMock,
|
|
}));
|
|
|
|
describe("CompanyForm", () => {
|
|
beforeEach(() => {
|
|
createMock.mockReset();
|
|
updateMock.mockReset();
|
|
toastErrorMock.mockReset();
|
|
toastSuccessMock.mockReset();
|
|
findCompanyDuplicatesMock.mockReset();
|
|
// Default: no duplicates found. Tests that need a duplicate
|
|
// override this with `mockResolvedValueOnce` / `mockResolvedValue`.
|
|
findCompanyDuplicatesMock.mockResolvedValue([]);
|
|
});
|
|
|
|
it("saves company email and telephone fields", async () => {
|
|
createMock.mockResolvedValue({ id: "co1" });
|
|
const onSaved = vi.fn();
|
|
const onOpenChange = vi.fn();
|
|
|
|
render(
|
|
<CompanyForm
|
|
open
|
|
onOpenChange={onOpenChange}
|
|
onSaved={onSaved}
|
|
/>
|
|
);
|
|
|
|
const form = screen.getByRole("dialog").querySelector("form")!;
|
|
fireEvent.change(within(form).getByPlaceholderText("e.g. Acme Corp"), {
|
|
target: { value: "Acme" },
|
|
});
|
|
fireEvent.change(within(form).getByPlaceholderText("accounts@acme.co.za"), {
|
|
target: { value: "accounts@acme.co.za" },
|
|
});
|
|
fireEvent.change(within(form).getByPlaceholderText("e.g. 011 555 0100"), {
|
|
target: { value: "011 555 0100" },
|
|
});
|
|
|
|
fireEvent.click(within(form).getByRole("button", { name: "Create Company" }));
|
|
|
|
await waitFor(() => {
|
|
expect(createMock).toHaveBeenCalledWith({
|
|
company: "Acme",
|
|
address: "",
|
|
vendor: "",
|
|
company_email: "accounts@acme.co.za",
|
|
company_tel: "011 555 0100",
|
|
coordinates: { lon: 0, lat: 0 },
|
|
});
|
|
});
|
|
});
|
|
|
|
it("blocks save when an exact-case-insensitive duplicate exists and lets the operator override", async () => {
|
|
// Simulate PB returning an existing record whose name matches the
|
|
// form value when both are lower-cased. The submit handler should
|
|
// pop the duplicate dialog instead of writing the record.
|
|
findCompanyDuplicatesMock.mockResolvedValue([
|
|
{ id: "co-existing", company: "ACME" } as never,
|
|
]);
|
|
const onSaved = vi.fn();
|
|
const onOpenChange = vi.fn();
|
|
|
|
render(
|
|
<CompanyForm open onOpenChange={onOpenChange} onSaved={onSaved} />
|
|
);
|
|
|
|
// Scope to the visible form — the base-ui Sheet/Dialog infrastructure
|
|
// leaves inert focus-guard nodes in the DOM that match a global
|
|
// `getByPlaceholderText` query, so we narrow the lookup to the
|
|
// <form> element the form renders.
|
|
const form = screen.getByRole("dialog").querySelector("form")!;
|
|
fireEvent.change(within(form).getByPlaceholderText("e.g. Acme Corp"), {
|
|
target: { value: "acme" },
|
|
});
|
|
fireEvent.click(within(form).getByRole("button", { name: "Create Company" }));
|
|
|
|
// Wait for the dialog to appear — that's the proof the block fired.
|
|
// The dialog renders the existing match.
|
|
await waitFor(() => {
|
|
expect(screen.getByText("ACME")).toBeInTheDocument();
|
|
});
|
|
// PB write was NOT called.
|
|
expect(createMock).not.toHaveBeenCalled();
|
|
|
|
// Click "Save anyway" — the override should land the write.
|
|
fireEvent.click(screen.getByRole("button", { name: /save anyway/i }));
|
|
await waitFor(() => {
|
|
expect(createMock).toHaveBeenCalledWith(
|
|
expect.objectContaining({ company: "acme" })
|
|
);
|
|
});
|
|
});
|
|
|
|
it("excludes the current record from the duplicate check in edit mode", async () => {
|
|
// The form is bound to an existing record. Typing the same name
|
|
// shouldn't flag itself. We assert that the helper was called with
|
|
// the record's id as `excludeId`.
|
|
const existing = { id: "co-self", company: "Acme" };
|
|
findCompanyDuplicatesMock.mockResolvedValue([]);
|
|
render(
|
|
<CompanyForm
|
|
open
|
|
onOpenChange={() => undefined}
|
|
onSaved={() => undefined}
|
|
company={existing as never}
|
|
/>
|
|
);
|
|
|
|
const form = screen.getByRole("dialog").querySelector("form")!;
|
|
fireEvent.change(within(form).getByPlaceholderText("e.g. Acme Corp"), {
|
|
target: { value: "Acme" },
|
|
});
|
|
|
|
// The effect debounces by 300ms, so we wait for the helper call
|
|
// to land with the record's id as the excludeId argument.
|
|
await waitFor(() => {
|
|
expect(findCompanyDuplicatesMock).toHaveBeenCalledWith(
|
|
"Acme",
|
|
"co-self"
|
|
);
|
|
});
|
|
});
|
|
});
|