This commit includes the temporary Next.js build cache files, public brand logos, and project configuration added in this change.
181 lines
6 KiB
TypeScript
181 lines
6 KiB
TypeScript
import { describe, expect, it, vi, beforeEach } from "vitest";
|
|
|
|
/**
|
|
* Unit tests for the duplicate-detection helpers added 2026-07-22.
|
|
*
|
|
* The helpers are split into two layers:
|
|
* 1. A pure fetch that pulls a small page of records whose `field`
|
|
* matches the trimmed value as a case-insensitive substring
|
|
* (via PB's `~` operator), then post-filters to an exact
|
|
* case-insensitive trimmed match.
|
|
* 2. The three public entry points — `findCompanyDuplicates`,
|
|
* `findSupplierDuplicates`, `findCustomerDuplicates` — that wire
|
|
* the right collection/field for each entity, and add the extra
|
|
* multi-field gate (name + company, plus optional email) for
|
|
* customers.
|
|
*
|
|
* These tests stub PocketBase at the `getPocketBase` boundary and
|
|
* assert on the filter string and the post-filtered result.
|
|
*/
|
|
|
|
const { getListMock, collectionMock } = vi.hoisted(() => {
|
|
const getListMock = vi.fn();
|
|
const collectionMock = vi.fn(() => ({ getList: getListMock }));
|
|
return { getListMock, collectionMock };
|
|
});
|
|
|
|
vi.mock("./pocketbase", () => ({
|
|
__esModule: true,
|
|
default: () => ({
|
|
collection: collectionMock,
|
|
}),
|
|
}));
|
|
|
|
import {
|
|
findCompanyDuplicates,
|
|
findCustomerDuplicates,
|
|
findSupplierDuplicates,
|
|
} from "./pb-helpers";
|
|
|
|
describe("findCompanyDuplicates", () => {
|
|
beforeEach(() => {
|
|
getListMock.mockReset();
|
|
collectionMock.mockClear();
|
|
});
|
|
|
|
it("returns an empty list when the value is blank", async () => {
|
|
const result = await findCompanyDuplicates(" ");
|
|
expect(result).toEqual([]);
|
|
expect(getListMock).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("queries PB with a case-insensitive substring filter and post-filters to exact match", async () => {
|
|
// PB returns both a substring match ("Acme") and a non-match
|
|
// ("Acme Subsidiary"). The helper should keep only the exact
|
|
// case-insensitive trimmed match ("ACME").
|
|
getListMock.mockResolvedValueOnce({
|
|
items: [
|
|
{ id: "c1", company: "Acme" },
|
|
{ id: "c2", company: "Acme Subsidiary" },
|
|
{ id: "c3", company: "ACME" },
|
|
],
|
|
});
|
|
const result = await findCompanyDuplicates("acme");
|
|
expect(getListMock).toHaveBeenCalledWith(
|
|
1,
|
|
20,
|
|
expect.objectContaining({ filter: 'company ~ "acme"' })
|
|
);
|
|
expect(result.map((c) => c.id)).toEqual(["c1", "c3"]);
|
|
});
|
|
|
|
it("appends `id != ...` to the filter when excludeId is set", async () => {
|
|
getListMock.mockResolvedValueOnce({ items: [] });
|
|
await findCompanyDuplicates("Acme", "co-self");
|
|
expect(getListMock).toHaveBeenCalledWith(
|
|
1,
|
|
20,
|
|
expect.objectContaining({
|
|
filter: 'company ~ "Acme" && id != "co-self"',
|
|
})
|
|
);
|
|
});
|
|
|
|
it("escapes double quotes in the value so PB doesn't blow up", async () => {
|
|
getListMock.mockResolvedValueOnce({ items: [] });
|
|
await findCompanyDuplicates('Acme "Pty" Ltd');
|
|
expect(getListMock).toHaveBeenCalledWith(
|
|
1,
|
|
20,
|
|
expect.objectContaining({
|
|
filter: 'company ~ "Acme \\"Pty\\" Ltd"',
|
|
})
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("findSupplierDuplicates", () => {
|
|
beforeEach(() => {
|
|
getListMock.mockReset();
|
|
collectionMock.mockClear();
|
|
});
|
|
|
|
it("queries the suppliers collection by suppliername", async () => {
|
|
getListMock.mockResolvedValueOnce({
|
|
items: [{ id: "s1", suppliername: "West Rand" }],
|
|
});
|
|
const result = await findSupplierDuplicates("WEST RAND", "s-self");
|
|
expect(collectionMock).toHaveBeenCalledWith("suppliers");
|
|
expect(getListMock).toHaveBeenCalledWith(
|
|
1,
|
|
20,
|
|
expect.objectContaining({
|
|
filter: 'suppliername ~ "WEST RAND" && id != "s-self"',
|
|
})
|
|
);
|
|
expect(result).toHaveLength(1);
|
|
});
|
|
});
|
|
|
|
describe("findCustomerDuplicates", () => {
|
|
beforeEach(() => {
|
|
getListMock.mockReset();
|
|
collectionMock.mockClear();
|
|
});
|
|
|
|
it("requires the same (name, company) pair to match", async () => {
|
|
// Two customers with the same name "John Smith" — one at Acme,
|
|
// one at MineCo. The form is filling out (John Smith, Acme),
|
|
// so only the Acme row should be a duplicate.
|
|
getListMock.mockResolvedValueOnce({
|
|
items: [
|
|
{ id: "cu1", customer: "John Smith", company: "acme-id", email: "" },
|
|
{ id: "cu2", customer: "John Smith", company: "mineco-id", email: "" },
|
|
],
|
|
});
|
|
const result = await findCustomerDuplicates("john smith", "acme-id", "", "self-id");
|
|
expect(collectionMock).toHaveBeenCalledWith("customers");
|
|
expect(getListMock).toHaveBeenCalledWith(
|
|
1,
|
|
50,
|
|
expect.objectContaining({
|
|
filter: 'customer ~ "john smith" && id != "self-id"',
|
|
expand: "company",
|
|
})
|
|
);
|
|
expect(result.map((c) => c.id)).toEqual(["cu1"]);
|
|
});
|
|
|
|
it("adds an email-gate when both records have an email set", async () => {
|
|
// Same name + same company, but different emails — these are
|
|
// genuinely two different people at the same company. The
|
|
// email-gate kicks in only when both sides have an email.
|
|
getListMock.mockResolvedValueOnce({
|
|
items: [
|
|
{ id: "cu1", customer: "John Smith", company: "acme-id", email: "john@acme.com" },
|
|
{ id: "cu2", customer: "John Smith", company: "acme-id", email: "different@acme.com" },
|
|
],
|
|
});
|
|
const result = await findCustomerDuplicates(
|
|
"John Smith",
|
|
"acme-id",
|
|
"john@acme.com",
|
|
"self-id"
|
|
);
|
|
// cu1 matches (name+company, emails equal), cu2 doesn't (different email).
|
|
expect(result.map((c) => c.id)).toEqual(["cu1"]);
|
|
});
|
|
|
|
it("skips the email gate when the form's email is blank", async () => {
|
|
// Form has no email, but the existing record does. We should
|
|
// still match on (name, company) alone — otherwise the form
|
|
// would miss every customer record that's missing an email.
|
|
getListMock.mockResolvedValueOnce({
|
|
items: [
|
|
{ id: "cu1", customer: "John Smith", company: "acme-id", email: "john@acme.com" },
|
|
],
|
|
});
|
|
const result = await findCustomerDuplicates("John Smith", "acme-id", "", "self-id");
|
|
expect(result).toHaveLength(1);
|
|
});
|
|
});
|