mirror of
https://github.com/Sea-Haven-Industries/shoc-frontend-new.git
synced 2026-09-30 06:53:12 +00:00
fix: complete SH-283 vendor QA paths (#148)
Co-authored-by: Codex Review Integration <codex-review@local.invalid>
This commit is contained in:
parent
3b1fe016fe
commit
7ec522e8ea
9 changed files with 103 additions and 8 deletions
22
e2e/vendors/vendors.spec.ts
vendored
22
e2e/vendors/vendors.spec.ts
vendored
|
|
@ -475,6 +475,28 @@ test.describe("Vendor directory prototype parity", () => {
|
|||
|
||||
await page.getByRole("combobox", { name: "Company (required)" }).click();
|
||||
await page.getByRole("option", { name: "Gateway Plumbing" }).click();
|
||||
const floatedCompanyLabel = page
|
||||
.getByRole("dialog", { name: /Add Vendor/ })
|
||||
.locator("label")
|
||||
.filter({ hasText: "Company (required)" });
|
||||
const floatedCompanyField = floatedCompanyLabel.locator("..");
|
||||
await expect(floatedCompanyLabel).toHaveText("Company (required)");
|
||||
await expect(floatedCompanyField.locator("legend")).toHaveText("Company (required)");
|
||||
const floatedMetrics = await floatedCompanyLabel.evaluate((element) => {
|
||||
const labelRect = element.getBoundingClientRect();
|
||||
const legendRect = element
|
||||
.closest(".MuiFormControl-root")
|
||||
?.querySelector("legend")
|
||||
?.getBoundingClientRect();
|
||||
return {
|
||||
labelClientWidth: element.clientWidth,
|
||||
labelScrollWidth: element.scrollWidth,
|
||||
legendWidth: legendRect?.width ?? 0,
|
||||
labelWidth: labelRect.width,
|
||||
};
|
||||
});
|
||||
expect(floatedMetrics.labelClientWidth).toBeGreaterThanOrEqual(floatedMetrics.labelScrollWidth);
|
||||
expect(floatedMetrics.legendWidth).toBeGreaterThanOrEqual(floatedMetrics.labelWidth);
|
||||
await expect(page.getByLabel("Company Phone (optional)")).toHaveValue("314-555-0100");
|
||||
await expect(page.getByRole("textbox", { name: "Email (optional)", exact: true })).toHaveValue(
|
||||
"dispatch@gateway.test",
|
||||
|
|
|
|||
|
|
@ -8,6 +8,15 @@ interface VendorRosterConflictAlertProps {
|
|||
}
|
||||
|
||||
export function VendorRosterConflictAlert({ conflict, onReload }: VendorRosterConflictAlertProps) {
|
||||
if (conflict.kind === "duplicate") {
|
||||
return (
|
||||
<Alert severity="warning">
|
||||
<AlertTitle>Company name already exists</AlertTitle>
|
||||
{conflict.message}
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
if (conflict.kind === "stale") {
|
||||
return (
|
||||
<Alert
|
||||
|
|
|
|||
|
|
@ -67,8 +67,7 @@ function CompanyNameField({
|
|||
{...field}
|
||||
label="Company (required)"
|
||||
placeholder="Start typing a company…"
|
||||
required
|
||||
slotProps={{ inputLabel: { required: false } }}
|
||||
slotProps={{ htmlInput: { required: true } }}
|
||||
error={Boolean(errors.name)}
|
||||
helperText={errors.name?.message}
|
||||
fullWidth
|
||||
|
|
@ -111,14 +110,13 @@ function CompanyNameField({
|
|||
<TextField
|
||||
{...params}
|
||||
label="Company (required)"
|
||||
required
|
||||
placeholder="Start typing a company…"
|
||||
error={Boolean(errors.name)}
|
||||
helperText={errors.name?.message}
|
||||
inputRef={field.ref}
|
||||
slotProps={{
|
||||
...params.slotProps,
|
||||
inputLabel: { ...params.slotProps?.inputLabel, required: false },
|
||||
htmlInput: { ...params.slotProps?.htmlInput, required: true },
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -179,8 +179,13 @@ export function mapRosterConflict(raw: unknown, fallbackMessage: string): Vendor
|
|||
const item = asRecord(raw);
|
||||
const blockedRaw = item.blockedWorkOrders ?? item.BlockedWorkOrders ?? item.openWorkOrders;
|
||||
const blockedWorkOrders = Array.isArray(blockedRaw) ? blockedRaw.map(mapBlockedWorkOrder) : [];
|
||||
const code = readString(item, "code", "Code");
|
||||
const kind: VendorRosterConflictKind =
|
||||
blockedWorkOrders.length > 0 ? "open-work-orders" : "stale";
|
||||
code === "duplicate_vendor_company_name"
|
||||
? "duplicate"
|
||||
: blockedWorkOrders.length > 0
|
||||
? "open-work-orders"
|
||||
: "stale";
|
||||
const message = readString(item, "message", "Message") || fallbackMessage;
|
||||
return { kind, message, blockedWorkOrders };
|
||||
}
|
||||
|
|
|
|||
2
src/domain/vendors/types/vendor.ts
vendored
2
src/domain/vendors/types/vendor.ts
vendored
|
|
@ -145,7 +145,7 @@ export interface VendorRosterBlockedWorkOrder {
|
|||
scheduledDate?: string;
|
||||
}
|
||||
|
||||
export type VendorRosterConflictKind = "open-work-orders" | "stale";
|
||||
export type VendorRosterConflictKind = "duplicate" | "open-work-orders" | "stale";
|
||||
|
||||
export interface VendorRosterConflict {
|
||||
kind: VendorRosterConflictKind;
|
||||
|
|
|
|||
|
|
@ -67,9 +67,12 @@ describe("VendorCreateModal validation", () => {
|
|||
|
||||
const company = screen.getByRole("combobox", { name: "Company (required)" });
|
||||
expect(company).toBeRequired();
|
||||
expect(screen.getByText("Company (required)")).toBeInTheDocument();
|
||||
expect(screen.getAllByText("Company (required)")).toHaveLength(2);
|
||||
expect(screen.queryByText("*")).not.toBeInTheDocument();
|
||||
expect(document.querySelector(".MuiFormLabel-asterisk")).toBeNull();
|
||||
expect(company.closest(".MuiFormControl-root")?.querySelector("legend")).toHaveTextContent(
|
||||
/^Company \(required\)$/,
|
||||
);
|
||||
});
|
||||
|
||||
it("labels the plain company field exactly, required, with no generated asterisk", () => {
|
||||
|
|
@ -77,9 +80,12 @@ describe("VendorCreateModal validation", () => {
|
|||
|
||||
const company = screen.getByRole("textbox", { name: "Company (required)" });
|
||||
expect(company).toBeRequired();
|
||||
expect(screen.getByText("Company (required)")).toBeInTheDocument();
|
||||
expect(screen.getAllByText("Company (required)")).toHaveLength(2);
|
||||
expect(screen.queryByText("*")).not.toBeInTheDocument();
|
||||
expect(document.querySelector(".MuiFormLabel-asterisk")).toBeNull();
|
||||
expect(company.closest(".MuiFormControl-root")?.querySelector("legend")).toHaveTextContent(
|
||||
/^Company \(required\)$/,
|
||||
);
|
||||
});
|
||||
|
||||
it("shows the phone-or-email error once without marking either input individually invalid", async () => {
|
||||
|
|
|
|||
22
src/test/app/(protected)/vendors/vendor-roster-conflict-alert.test.tsx
vendored
Normal file
22
src/test/app/(protected)/vendors/vendor-roster-conflict-alert.test.tsx
vendored
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
import { render, screen } from "@testing-library/react";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { VendorRosterConflictAlert } from "@/app/(protected)/vendors/_components/vendor-roster-conflict-alert";
|
||||
|
||||
describe("VendorRosterConflictAlert", () => {
|
||||
it("explains a duplicate company without offering a stale-data reload", () => {
|
||||
render(
|
||||
<VendorRosterConflictAlert
|
||||
conflict={{
|
||||
kind: "duplicate",
|
||||
message: "Another vendor company already uses that name.",
|
||||
blockedWorkOrders: [],
|
||||
}}
|
||||
onReload={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByText("Company name already exists")).toBeInTheDocument();
|
||||
expect(screen.getByText("Another vendor company already uses that name.")).toBeInTheDocument();
|
||||
expect(screen.queryByRole("button", { name: "Reload" })).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
|
@ -118,6 +118,25 @@ describe("vendorCompanyRosterApi", () => {
|
|||
);
|
||||
});
|
||||
|
||||
it("throws a duplicate conflict for the stable create response", async () => {
|
||||
apiPost.mockRejectedValueOnce(
|
||||
httpError(409, {
|
||||
code: "duplicate_vendor_company_name",
|
||||
message: "Another vendor company already uses that name.",
|
||||
}),
|
||||
);
|
||||
|
||||
await expect(vendorCompanyRosterApi.create({ name: "Solo Co" })).rejects.toSatisfy(
|
||||
(error: unknown) => {
|
||||
if (!isVendorRosterConflictError(error)) return false;
|
||||
return (
|
||||
error.conflict.kind === "duplicate" &&
|
||||
error.conflict.message === "Another vendor company already uses that name."
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it("rethrows non-conflict errors untouched", async () => {
|
||||
const generic = new Error("boom");
|
||||
apiPost.mockRejectedValueOnce(generic);
|
||||
|
|
|
|||
|
|
@ -113,6 +113,20 @@ describe("vendor roster mapper", () => {
|
|||
expect(conflict.message).toBe("stale rowversion");
|
||||
});
|
||||
|
||||
it("classifies the stable duplicate-company response", () => {
|
||||
const conflict = mapRosterConflict(
|
||||
{
|
||||
code: "duplicate_vendor_company_name",
|
||||
message: "Another vendor company already uses that name.",
|
||||
},
|
||||
"fallback",
|
||||
);
|
||||
|
||||
expect(conflict.kind).toBe("duplicate");
|
||||
expect(conflict.message).toBe("Another vendor company already uses that name.");
|
||||
expect(conflict.blockedWorkOrders).toEqual([]);
|
||||
});
|
||||
|
||||
it("preserves an explicit Phone/Email/Text preferred contact on read", () => {
|
||||
expect(
|
||||
mapRosterTechnician({ contactName: "A", preferredContact: "Phone" }).preferredContact,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue