From 5e91a5d7c0db48f62f5fea95ae32721a3d53c6b3 Mon Sep 17 00:00:00 2001 From: SJCoetz Date: Fri, 31 Jul 2026 13:58:05 +0200 Subject: [PATCH] test(stock-orders): QuickDraftModal edit-mode coverage Asserts the modal hides non-targeted drafts, renders qty/cost inputs per line, PATCHes only changed fields on Save, and that remove still works in edit mode. Co-authored-by: Orca --- .../stock-orders/quick-draft-modal.test.tsx | 292 ++++++++++++++++++ 1 file changed, 292 insertions(+) diff --git a/src/components/stock-orders/quick-draft-modal.test.tsx b/src/components/stock-orders/quick-draft-modal.test.tsx index 904a08f..9c372f0 100644 --- a/src/components/stock-orders/quick-draft-modal.test.tsx +++ b/src/components/stock-orders/quick-draft-modal.test.tsx @@ -616,3 +616,295 @@ describe("QuickDraftModal — no-history fallback", () => { ).toBeInTheDocument(); }); }); + +describe("QuickDraftModal — edit mode", () => { + beforeEach(() => { + vi.useFakeTimers(); + }); + + afterEach(() => { + vi.useRealTimers(); + }); + + it("renders only the targeted draft and hides the others", async () => { + global.fetch = vi.fn().mockResolvedValue({ + ok: true, + json: async () => ({ + ok: true, + items: [ + { + id: "po-1", + purchase_number: "PO26073001STK", + supplier: "sup-1", + supplier_name: "Steel & Pipe Co", + lines: [ + { + id: "line-1", + description: "115mm Cutting Disc", + quantity: 50, + cost_price: 20, + }, + ], + }, + { + id: "po-2", + purchase_number: "PO26073002STK", + supplier: "sup-2", + supplier_name: "Hardware Hub", + lines: [ + { + id: "line-3", + description: "Bolt pack", + quantity: 5, + cost_price: 100, + }, + ], + }, + ], + }), + }); + + render( + {}} + editPurchaseId="po-1" + />, + ); + await act(async () => { + await vi.advanceTimersByTimeAsync(0); + }); + + expect(screen.getByText(/Steel & Pipe Co/)).toBeInTheDocument(); + expect(screen.queryByText(/Hardware Hub/)).not.toBeInTheDocument(); + }); + + it("renders edit-mode qty + cost inputs for each line of the targeted draft", async () => { + global.fetch = vi.fn().mockResolvedValue({ + ok: true, + json: async () => ({ + ok: true, + items: [ + { + id: "po-1", + purchase_number: "PO26073001STK", + supplier: "sup-1", + supplier_name: "Steel & Pipe Co", + lines: [ + { + id: "line-1", + description: "115mm Cutting Disc", + quantity: 50, + cost_price: 20, + }, + ], + }, + ], + }), + }); + + render( + {}} + editPurchaseId="po-1" + />, + ); + await act(async () => { + await vi.advanceTimersByTimeAsync(0); + }); + + const qtyInput = screen.getByTestId( + "quick-draft-edit-qty-line-1", + ) as HTMLInputElement; + expect(qtyInput).toBeInTheDocument(); + expect(qtyInput.value).toBe("50"); + + const costInput = screen.getByTestId( + "quick-draft-edit-cost-line-1", + ) as HTMLInputElement; + expect(costInput).toBeInTheDocument(); + expect(costInput.value).toBe("20"); + }); + + it("Save PATCHes only the changed fields and refetches cards", async () => { + const { toast } = await import("sonner"); + const successSpy = vi + .spyOn(toast, "success") + .mockImplementation(() => ""); + + global.fetch = vi + .fn() + // initial cards fetch + .mockResolvedValueOnce({ + ok: true, + json: async () => ({ + ok: true, + items: [ + { + id: "po-1", + purchase_number: "PO26073001STK", + supplier: "sup-1", + supplier_name: "Steel & Pipe Co", + lines: [ + { + id: "line-1", + description: "115mm Cutting Disc", + quantity: 50, + cost_price: 20, + }, + ], + }, + ], + }), + }) + // PATCH response + .mockResolvedValueOnce({ + ok: true, + json: async () => ({ ok: true }), + }) + // refetch cards after save + .mockResolvedValueOnce({ + ok: true, + json: async () => ({ + ok: true, + items: [ + { + id: "po-1", + purchase_number: "PO26073001STK", + supplier: "sup-1", + supplier_name: "Steel & Pipe Co", + lines: [ + { + id: "line-1", + description: "115mm Cutting Disc", + quantity: 75, + cost_price: 20, + }, + ], + }, + ], + }), + }); + + render( + {}} + editPurchaseId="po-1" + />, + ); + await act(async () => { + await vi.advanceTimersByTimeAsync(0); + }); + + const qtyInput = screen.getByTestId( + "quick-draft-edit-qty-line-1", + ) as HTMLInputElement; + fireEvent.change(qtyInput, { target: { value: "75" } }); + fireEvent.click(screen.getByTestId("quick-draft-save-line-line-1")); + + await act(async () => { + await vi.advanceTimersByTimeAsync(0); + }); + + const patchCalls = ( + global.fetch as ReturnType + ).mock.calls.filter( + (c) => typeof c[0] === "string" && c[0].includes("/line/line-1"), + ); + expect(patchCalls.length).toBeGreaterThan(0); + expect(patchCalls[0][1].method).toBe("PATCH"); + const body = JSON.parse(patchCalls[0][1].body); + expect(body).toEqual({ quantity: 75 }); + expect(successSpy).toHaveBeenCalledWith("Line updated"); + successSpy.mockRestore(); + }); + + it("remove still works in edit mode", async () => { + global.fetch = vi + .fn() + // initial cards fetch + .mockResolvedValueOnce({ + ok: true, + json: async () => ({ + ok: true, + items: [ + { + id: "po-1", + purchase_number: "PO26073001STK", + supplier: "sup-1", + supplier_name: "Steel & Pipe Co", + lines: [ + { + id: "line-1", + description: "115mm Cutting Disc", + quantity: 50, + cost_price: 20, + }, + { + id: "line-2", + description: "Angle grinder", + quantity: 1, + cost_price: 1500, + }, + ], + }, + ], + }), + }) + // DELETE response + .mockResolvedValueOnce({ + ok: true, + json: async () => ({ ok: true }), + }) + // refetch cards after delete + .mockResolvedValueOnce({ + ok: true, + json: async () => ({ + ok: true, + items: [ + { + id: "po-1", + purchase_number: "PO26073001STK", + supplier: "sup-1", + supplier_name: "Steel & Pipe Co", + lines: [ + { + id: "line-1", + description: "115mm Cutting Disc", + quantity: 50, + cost_price: 20, + }, + ], + }, + ], + }), + }); + + render( + {}} + editPurchaseId="po-1" + />, + ); + await act(async () => { + await vi.advanceTimersByTimeAsync(0); + }); + + fireEvent.click(screen.getByTestId("quick-draft-remove-line-line-2")); + + await act(async () => { + await vi.advanceTimersByTimeAsync(0); + }); + + const deleteCalls = ( + global.fetch as ReturnType + ).mock.calls.filter( + (c) => typeof c[0] === "string" && c[0].includes("/line/line-2"), + ); + expect(deleteCalls.length).toBe(1); + expect(deleteCalls[0][1].method).toBe("DELETE"); + }); +});