shoc-frontend-new/src/test/domain/vendors/api/vendors-api.test.ts
Alexandre Brandizzi 30c43a4d86
SH-281: group vendor directory rows by company (#188)
* fix(vendors): add accessible trade overflow tooltip

* fix(vendors): add accessible trade overflow tooltip

* refactor(vendors): satisfy row maintainability gate

* feat(vendors): group directory rows by company

* chore(vendors): trigger SH-281 review checks

* test(vendors): align company directory visuals

* test(vendors): complete company visual baselines

* fix(vendors): keep empty states in viewport

* test(vendors): align centered empty baseline

* fix(vendors): use grouped directory endpoint

* test(vendors): follow grouped directory endpoint

* test(vendors): refresh combined visual snapshots

---------

Co-authored-by: Codex Review Integration <codex-review@local.invalid>
2026-09-16 14:40:24 -03:00

421 lines
13 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { beforeEach, describe, expect, it, vi } from "vitest";
import { API_PATHS } from "@/api/api-paths";
const apiGet = vi.fn();
const apiPost = vi.fn();
const apiPut = vi.fn();
const apiDelete = vi.fn();
vi.mock("@/api/api", () => ({
apiGet: (...args: unknown[]) => apiGet(...args),
apiPost: (...args: unknown[]) => apiPost(...args),
apiPut: (...args: unknown[]) => apiPut(...args),
apiDelete: (...args: unknown[]) => apiDelete(...args),
}));
import { vendorsApi } from "@/domain/vendors/api/vendors-api";
describe("vendorsApi.getList", () => {
beforeEach(() => {
apiGet.mockReset();
apiPost.mockReset();
apiPut.mockReset();
apiDelete.mockReset();
apiGet.mockResolvedValue({
Data: [
{
Id: 7,
CompanyId: "co-1",
CompanyName: "Gateway Plumbing",
ContactName: "Adam Whyte",
Email: "dispatch@gateway.test",
Phone: "314-555-0198",
Address: "1 Market St",
City: "St. Louis",
State: "MO",
IsActive: false,
PreferredContact: "Email",
Technicians: [
{
Id: 7,
ContactName: "Adam Whyte",
Email: "adam@gateway.test",
Phone: "314-555-0198",
TradeSpecialties: "Plumbing",
IsActive: false,
TotalJobs: 3,
},
],
},
],
TotalCount: 1,
});
});
it("forwards the status filter and maps list location fields", async () => {
const result = await vendorsApi.getList({ search: "gateway", isActive: false, page: 1 });
expect(apiGet).toHaveBeenCalledWith(API_PATHS.vendor.getList, {
searchParams: { search: "gateway", isActive: "false", page: 1, pageSize: 12 },
});
expect(result.items[0]).toMatchObject({
address: "1 Market St",
city: "St. Louis",
state: "MO",
isActive: false,
companyId: "co-1",
preferredContact: "Email",
technicians: [
expect.objectContaining({
id: 7,
email: "adam@gateway.test",
phone: "314-555-0198",
totalJobs: 3,
}),
],
});
});
it("synthesizes one technician for a legacy ungrouped response", async () => {
apiGet.mockResolvedValueOnce({
Data: [
{
Id: 9,
CompanyName: "Legacy Co",
ContactName: "Legacy Tech",
Email: "legacy@example.test",
Phone: "555-0109",
IsActive: true,
},
],
TotalCount: 1,
});
const result = await vendorsApi.getList();
expect(result.items[0]?.technicians).toEqual([
expect.objectContaining({ id: 9, contactName: "Legacy Tech", phone: "555-0109" }),
]);
});
it("forwards all selected facets as repeated indexed query parameters", async () => {
await vendorsApi.getList({
companies: ["Gateway Plumbing", "Metro HVAC"],
trades: ["Plumbing"],
locations: ["St. Louis, MO"],
jobBuckets: ["50-99"],
});
expect(apiGet).toHaveBeenCalledWith(API_PATHS.vendor.getList, {
searchParams: {
search: "",
page: 1,
pageSize: 12,
"companies[0]": "Gateway Plumbing",
"companies[1]": "Metro HVAC",
"trades[0]": "Plumbing",
"locations[0]": "St. Louis, MO",
"jobBuckets[0]": "50-99",
},
});
});
it("maps company profiles and labels returned by the facets endpoint", async () => {
apiGet.mockResolvedValueOnce({
data: {
companies: [
{
name: "Gateway Plumbing",
companyPhone: "314-555-0100",
email: "dispatch@gateway.test",
address: "1 Market St",
city: "St. Louis",
state: "MO",
zip: "63101",
googleMapsUrl: "https://maps.google.com/gateway",
},
],
trades: ["Plumbing"],
locations: [{ city: "St. Louis", state: "MO" }],
jobBuckets: [{ id: "50-99", label: "50–99" }],
},
});
const facets = await vendorsApi.getFacets();
expect(apiGet).toHaveBeenCalledWith(API_PATHS.vendor.facets, { searchParams: {} });
expect(facets.companies[0]).toMatchObject({
name: "Gateway Plumbing",
companyPhone: "314-555-0100",
googleMapsUrl: "https://maps.google.com/gateway",
});
expect(facets.locations[0]?.label).toBe("St. Louis, MO");
expect(facets.jobBuckets[0]).toEqual({ id: "50-99", label: "50–99" });
});
it("serializes the requested facet scope as an isActive query parameter", async () => {
apiGet.mockResolvedValue({
data: { companies: [], trades: [], locations: [], jobBuckets: [] },
});
await vendorsApi.getFacets({ isActive: true });
await vendorsApi.getFacets({ isActive: false });
expect(apiGet).toHaveBeenNthCalledWith(1, API_PATHS.vendor.facets, {
searchParams: { isActive: "true" },
});
expect(apiGet).toHaveBeenNthCalledWith(2, API_PATHS.vendor.facets, {
searchParams: { isActive: "false" },
});
});
it("sends all editable prototype fields when creating and updating", async () => {
const values = {
companyId: "42",
companyName: "Gateway Plumbing",
contactName: "Adam Whyte",
phone: "+1 314-555-0198",
companyPhone: "314-555-0100",
email: "dispatch@gateway.test",
address: "1 Market St",
city: "St. Louis",
state: "MO",
zip: "63101",
tradeSpecialties: "Plumbing, HVAC",
googleMapsUrl: "https://maps.google.com/gateway",
notes: "Preferred after-hours vendor",
totalJobs: 72,
isActive: true,
};
apiPost.mockResolvedValueOnce({ data: values });
apiPut.mockResolvedValueOnce({ data: { ...values, isActive: false } });
await vendorsApi.create(values);
await vendorsApi.update(7, { ...values, isActive: false });
expect(apiPost).toHaveBeenCalledWith(
API_PATHS.rest.vendors,
expect.objectContaining({
companyName: "Gateway Plumbing",
notes: values.notes,
companyId: 42,
phone: "(314) 555-0198",
companyPhone: "(314) 555-0100",
}),
);
expect(apiPost.mock.calls[0]?.[1]).not.toHaveProperty("preferredContact");
expect(apiPut).toHaveBeenCalledWith(
`${API_PATHS.rest.vendors}/7`,
expect.objectContaining({ companyName: "Gateway Plumbing", isActive: false }),
);
expect(apiPut.mock.calls[0]?.[1]).not.toHaveProperty("preferredContact");
expect(apiPost.mock.calls[0]?.[1]).not.toHaveProperty("totalJobs");
expect(apiPut.mock.calls[0]?.[1]).not.toHaveProperty("totalJobs");
});
it("sends a null companyId when free-text company has no company link", async () => {
apiPost.mockResolvedValueOnce({ data: {} });
await vendorsApi.create({
companyName: "Independent Vendor LLC",
contactName: "Indy Tech",
});
expect(apiPost).toHaveBeenCalledWith(
API_PATHS.rest.vendors,
expect.objectContaining({ companyName: "Independent Vendor LLC", companyId: null }),
);
});
it("does not send a non-numeric company id to the integer backend contract", async () => {
apiPost.mockResolvedValueOnce({ data: {} });
await vendorsApi.create({
companyId: "co-1",
companyName: "Gateway Plumbing",
contactName: "Adam Whyte",
});
expect(apiPost).toHaveBeenCalledWith(
API_PATHS.rest.vendors,
expect.objectContaining({ companyId: null }),
);
});
it("preserves a legacy phone on read and omits it from an unrelated update", async () => {
apiGet.mockResolvedValueOnce({
data: {
id: 7,
companyName: "Legacy Vendor",
contactName: "Taylor",
phone: "555-1234 ext 9",
},
});
apiPut.mockResolvedValueOnce({ data: {} });
const vendor = await vendorsApi.getById(7);
expect(vendor.phone).toBe("555-1234 ext 9");
await vendorsApi.update(7, { notes: "Updated only the notes" });
expect(apiPut).toHaveBeenCalledWith(`${API_PATHS.rest.vendors}/7`, {
notes: "Updated only the notes",
});
});
it("sends only changed fields and canonicalizes a supplied phone", async () => {
apiPut.mockResolvedValueOnce({ data: {} });
await vendorsApi.update(7, {
isActive: false,
companyId: null,
phone: "314-555-0198",
});
expect(apiPut).toHaveBeenCalledWith(`${API_PATHS.rest.vendors}/7`, {
isActive: false,
companyId: null,
phone: "(314) 555-0198",
});
});
it("maps the deactivation-impact payload including linked work orders", async () => {
apiGet.mockResolvedValueOnce({
data: {
vendorId: 7,
canDeactivate: false,
openWorkOrders: [
{
workOrderId: 101,
workOrderNumber: "WO-101",
workOrderTitle: "Boiler repair",
status: "Dispatched",
scheduledDate: "2025-01-02",
dispatchId: 501,
},
],
},
});
const impact = await vendorsApi.getDeactivationImpact(7);
expect(apiGet).toHaveBeenCalledWith(
`${API_PATHS.rest.vendors}/7/${API_PATHS.vendor.deactivationImpact}`,
);
expect(impact).toEqual({
vendorId: 7,
canDeactivate: false,
openWorkOrders: [
{
workOrderId: 101,
workOrderNumber: "WO-101",
workOrderTitle: "Boiler repair",
status: "Dispatched",
scheduledDate: "2025-01-02",
dispatchId: 501,
},
],
});
});
it("deletes a vendor by id and surfaces the 409 error without retry swallowing", async () => {
apiDelete.mockResolvedValueOnce(undefined);
await vendorsApi.delete(7);
expect(apiDelete).toHaveBeenCalledWith(`${API_PATHS.rest.vendors}/7`);
const conflict = Object.assign(new Error("Conflict"), { response: { status: 409 } });
apiDelete.mockRejectedValueOnce(conflict);
await expect(vendorsApi.delete(8)).rejects.toThrow("Conflict");
});
});
describe("vendorsApi.getDirectoryList", () => {
beforeEach(() => {
apiGet.mockReset();
apiGet.mockResolvedValue({
Data: [
{
Id: 7,
CompanyId: "co-1",
CompanyName: "Gateway Plumbing",
ContactName: "Adam Whyte",
Email: "dispatch@gateway.test",
Phone: "314-555-0198",
Address: "1 Market St",
City: "St. Louis",
State: "MO",
IsActive: false,
PreferredContact: "Email",
Technicians: [
{
Id: 7,
ContactName: "Adam Whyte",
Email: "adam@gateway.test",
Phone: "314-555-0198",
TradeSpecialties: "Plumbing",
IsActive: false,
TotalJobs: 3,
},
],
},
],
TotalCount: 1,
});
});
it("targets the dedicated directory endpoint while getList keeps the legacy route", async () => {
await vendorsApi.getDirectoryList({ search: "gateway", isActive: false, page: 1 });
await vendorsApi.getList({ search: "gateway", isActive: false, page: 1 });
expect(API_PATHS.vendor.getDirectoryList).toBe("Vendor/GetVendorDirectoryList");
expect(API_PATHS.vendor.getList).toBe("Vendor/GetVendorList");
expect(apiGet).toHaveBeenNthCalledWith(1, API_PATHS.vendor.getDirectoryList, {
searchParams: { search: "gateway", isActive: "false", page: 1, pageSize: 12 },
});
expect(apiGet).toHaveBeenNthCalledWith(2, API_PATHS.vendor.getList, {
searchParams: { search: "gateway", isActive: "false", page: 1, pageSize: 12 },
});
});
it("builds directory query parameters with the same repeated facet encoding", async () => {
await vendorsApi.getDirectoryList({
companies: ["Gateway Plumbing", "Metro HVAC"],
trades: ["Plumbing"],
locations: ["St. Louis, MO"],
jobBuckets: ["50-99"],
});
expect(apiGet).toHaveBeenCalledWith(API_PATHS.vendor.getDirectoryList, {
searchParams: {
search: "",
page: 1,
pageSize: 12,
"companies[0]": "Gateway Plumbing",
"companies[1]": "Metro HVAC",
"trades[0]": "Plumbing",
"locations[0]": "St. Louis, MO",
"jobBuckets[0]": "50-99",
},
});
});
it("maps grouped company rows with the shared grouped-row mapper", async () => {
const result = await vendorsApi.getDirectoryList({ isActive: false, page: 1 });
expect(result.items[0]).toMatchObject({
companyId: "co-1",
companyName: "Gateway Plumbing",
address: "1 Market St",
city: "St. Louis",
state: "MO",
isActive: false,
technicians: [
expect.objectContaining({
id: 7,
contactName: "Adam Whyte",
email: "adam@gateway.test",
phone: "314-555-0198",
totalJobs: 3,
}),
],
});
});
});