import { describe, it, expect, beforeEach, vi } from "vitest"; import { fireEvent, render, screen, waitFor } from "@testing-library/react"; import { OrderSprintProcessModal } from "./order-sprint-process-modal"; // Mock the client PB so we can assert on the update payload written // when the operator clicks "Process Item". The modal also reads price // list + order history in `useEffect` — we stub those with empty // results so the test focuses purely on the save behaviour. const { updateMock, getListMock, supplierSearchStub, fetchMock } = vi.hoisted(() => ({ updateMock: vi.fn(), getListMock: vi.fn(), supplierSearchStub: vi.fn(), fetchMock: vi.fn(), })); vi.mock("@/lib/pocketbase", () => ({ __esModule: true, default: () => ({ collection: () => ({ getList: (...args: unknown[]) => getListMock(...args), update: updateMock, }), }), })); // Replace SupplierSearch with a plain onValueChange(e.target.value)} > {suppliers.map((s) => ( ))} ), })); void supplierSearchStub; // silence unused const baseItem = { id: "item-1", description: "HSS drill bit", quantity: 2, order_qty: 10, selling_price: 50, supplier: "", cost_price: 0, markup: 0, sku: "", notes: "", category: "", po_description: "", item_status: "pending", ai_item_insights: null, }; const suppliers = [ { id: "sup-1", suppliername: "Steel Co" }, { id: "sup-2", suppliername: "Bolt Suppliers" }, ]; function renderModal(props: Partial> = {}) { return render( , ); } async function setField(labelOrPlaceholder: string, value: string) { // The pricing inputs are wired with htmlFor/id; the other text // inputs use placeholders. Prefer the label, fall back to the // placeholder. let input: HTMLElement; try { input = screen.getByLabelText(labelOrPlaceholder, { exact: false }); } catch { input = screen.getByPlaceholderText(labelOrPlaceholder); } fireEvent.change(input, { target: { value } }); } describe("OrderSprintProcessModal", () => { beforeEach(() => { updateMock.mockReset(); updateMock.mockResolvedValue({}); // Order history + price list fetches resolve to empty lists so // the modal mounts without throwing. We don't assert on them. getListMock.mockResolvedValue({ items: [], totalItems: 0, totalPages: 1 }); // The context panel + margin-approval endpoint both go through // the global fetch. Route by URL so each gets a valid body — // returning the approval payload for the context call would // crash the panel's noData computation. fetchMock.mockImplementation(async (input: RequestInfo | URL) => { const url = typeof input === "string" ? input : input.toString(); if (url.includes("/context")) { return new Response( JSON.stringify({ generated_at: "2026-08-07T00:00:00Z", match: { sku: "", match_kind: "none", match_confidence: 0 }, last_quote: null, last_order: null, last_invoiced: null, stock: null, suppliers: [], cheapest: null, }), { status: 200, headers: { "content-type": "application/json" } }, ); } return new Response(JSON.stringify({ webhookFired: true }), { status: 200, headers: { "content-type": "application/json" }, }); }); vi.stubGlobal("fetch", fetchMock); }); it("writes item_status='processed' when supplier + cost + po_description are all set", async () => { renderModal(); // supplier fireEvent.change(screen.getByLabelText("Supplier"), { target: { value: "sup-1" }, }); // cost await setField("Cost (ZAR)", "25"); // po_description (uses placeholder because there's no