shoc-frontend-new/src/test/app/(protected)/vendors/vendor-create-modal.test.tsx
Alexandre Brandizzi 7ec522e8ea
Some checks are pending
Frontend checks / Build and test (push) Waiting to run
Frontend checks / governance (push) Waiting to run
Frontend checks / Visual regression (push) Waiting to run
Deploy / deploy (push) Waiting to run
fix: complete SH-283 vendor QA paths (#148)
Co-authored-by: Codex Review Integration <codex-review@local.invalid>
2026-08-27 15:03:36 -03:00

161 lines
6.8 KiB
TypeScript

import { fireEvent, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { describe, expect, it, vi } from "vitest";
import { VendorCreateModal } from "@/app/(protected)/vendors/_components/vendor-create-modal";
import { VendorRosterFormFields } from "@/app/(protected)/vendors/_components/vendor-roster-form-fields";
import {
VENDOR_COMPANY_CONTACT_REQUIRED_MESSAGE,
emptyVendorCompanyRosterForm,
vendorCompanyRosterSchema,
type VendorCompanyRosterFormValues,
} from "@/domain/vendors/schemas/vendor-roster-schema";
import { renderWithProviders } from "@/test/test-utils";
vi.mock("@/domain/vendors/use-cases/use-vendor-facets", () => ({
useVendorFacets: () => ({ data: { companies: [], trades: [] }, isLoading: false }),
}));
vi.mock("@/domain/vendors/use-cases/use-save-vendor-company-roster", async () => {
const actual = await vi.importActual<
typeof import("@/domain/vendors/use-cases/use-save-vendor-company-roster")
>("@/domain/vendors/use-cases/use-save-vendor-company-roster");
return {
...actual,
useSaveVendorCompanyRoster: () => ({ mutate: vi.fn(), isPending: false }),
};
});
function PlainCompanyFieldsHarness() {
const { control, handleSubmit, formState } = useForm<VendorCompanyRosterFormValues>({
defaultValues: emptyVendorCompanyRosterForm,
resolver: zodResolver(vendorCompanyRosterSchema),
mode: "onChange",
});
return (
<form onSubmit={handleSubmit(() => {})}>
<VendorRosterFormFields control={control} errors={formState.errors} />
</form>
);
}
function renderCreateModal() {
renderWithProviders(<VendorCreateModal open={true} onClose={vi.fn()} />, {
route: "/vendors",
withAuth: false,
});
}
async function fillCompanyNameAndSubmit(name = "Gateway Plumbing") {
fireEvent.change(screen.getByRole("combobox", { name: "Company (required)" }), {
target: { value: name },
});
await userEvent.click(screen.getByRole("button", { name: "Add Vendor" }));
}
describe("VendorCreateModal validation", () => {
it("explains the company phone-or-email requirement after submission", async () => {
renderCreateModal();
await fillCompanyNameAndSubmit();
expect(await screen.findByText(VENDOR_COMPANY_CONTACT_REQUIRED_MESSAGE)).toBeInTheDocument();
});
it("labels the autocomplete company field exactly, required, with no generated asterisk", () => {
renderCreateModal();
const company = screen.getByRole("combobox", { name: "Company (required)" });
expect(company).toBeRequired();
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", () => {
renderWithProviders(<PlainCompanyFieldsHarness />, { route: "/vendors", withAuth: false });
const company = screen.getByRole("textbox", { name: "Company (required)" });
expect(company).toBeRequired();
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 () => {
renderCreateModal();
await fillCompanyNameAndSubmit();
const message = await screen.findByText(VENDOR_COMPANY_CONTACT_REQUIRED_MESSAGE);
expect(message).toHaveAttribute("id");
expect(screen.getAllByText(VENDOR_COMPANY_CONTACT_REQUIRED_MESSAGE)).toHaveLength(1);
const phone = screen.getByRole("textbox", { name: "Company Phone (optional)" });
const email = screen.getByRole("textbox", { name: "Email (optional)" });
expect(phone).not.toHaveAttribute("aria-invalid", "true");
expect(email).not.toHaveAttribute("aria-invalid", "true");
expect(phone.getAttribute("aria-describedby")).toContain(message.getAttribute("id"));
expect(email.getAttribute("aria-describedby")).toContain(message.getAttribute("id"));
});
it("marks only the phone input invalid when the phone is malformed", async () => {
renderCreateModal();
await fillCompanyNameAndSubmit();
const phone = screen.getByRole("textbox", { name: "Company Phone (optional)" });
const email = screen.getByRole("textbox", { name: "Email (optional)" });
await userEvent.type(phone, "314");
await userEvent.click(screen.getByRole("button", { name: "Add Vendor" }));
expect(await screen.findByText("Enter a 10-digit phone number")).toBeInTheDocument();
expect(phone).toHaveAttribute("aria-invalid", "true");
expect(email).not.toHaveAttribute("aria-invalid", "true");
expect(screen.queryByText(VENDOR_COMPANY_CONTACT_REQUIRED_MESSAGE)).not.toBeInTheDocument();
});
it("marks only the email input invalid when the email is malformed", async () => {
renderCreateModal();
await fillCompanyNameAndSubmit();
const phone = screen.getByRole("textbox", { name: "Company Phone (optional)" });
const email = screen.getByRole("textbox", { name: "Email (optional)" });
await userEvent.type(email, "not-an-email");
await userEvent.click(screen.getByRole("button", { name: "Add Vendor" }));
expect(await screen.findByText("Invalid email")).toBeInTheDocument();
expect(email).toHaveAttribute("aria-invalid", "true");
expect(phone).not.toHaveAttribute("aria-invalid", "true");
expect(screen.queryByText(VENDOR_COMPANY_CONTACT_REQUIRED_MESSAGE)).not.toBeInTheDocument();
});
it("clears the phone-or-email error once a valid phone is entered", async () => {
renderCreateModal();
await fillCompanyNameAndSubmit();
const phone = screen.getByRole("textbox", { name: "Company Phone (optional)" });
expect(await screen.findByText(VENDOR_COMPANY_CONTACT_REQUIRED_MESSAGE)).toBeInTheDocument();
await userEvent.type(phone, "3145550100");
await waitFor(() =>
expect(screen.queryByText(VENDOR_COMPANY_CONTACT_REQUIRED_MESSAGE)).not.toBeInTheDocument(),
);
});
it("shows the notes limit and live character counter", async () => {
renderCreateModal();
const notes = screen.getByRole("textbox", { name: "Notes (optional)" });
fireEvent.change(notes, { target: { value: "abc" } });
expect(notes).toHaveAttribute("maxlength", "500");
await waitFor(() =>
expect(notes).toHaveAccessibleDescription(/3\/500 characters · Maximum 500 characters/),
);
});
});