From c8e977c61ed1118c99550a059a61eb273126e6f4 Mon Sep 17 00:00:00 2001 From: Dave Date: Fri, 16 Oct 2026 19:48:19 +0200 Subject: [PATCH] fix(quote-assistant): warn-coloured doubts, real button for no-match badge, more button tests Co-Authored-By: Claude Opus 5.5 --- src/components/ai-fill-marker.test.tsx | 7 +++-- src/components/ai-fill-marker.tsx | 19 +++++++------- src/components/ai-fill-quote-button.test.tsx | 27 ++++++++++++++++++++ 3 files changed, 41 insertions(+), 12 deletions(-) diff --git a/src/components/ai-fill-marker.test.tsx b/src/components/ai-fill-marker.test.tsx index 130a806..e232a91 100644 --- a/src/components/ai-fill-marker.test.tsx +++ b/src/components/ai-fill-marker.test.tsx @@ -68,6 +68,9 @@ describe("AiFillMarker", () => { expect(screen.getByText("Same size and finish")).toBeInTheDocument(); expect(screen.getByText("Length not stated")).toBeInTheDocument(); expect(screen.getByText("Could be stainless")).toBeInTheDocument(); + // Doubts are highlighted in the warning colour. + expect(screen.getByText("Doubts").className).toContain("text-warn"); + expect(screen.getByText("Length not stated").closest("ul")?.className).toContain("text-warn"); expect(screen.queryByText("Selling price from default markup")).not.toBeInTheDocument(); }); @@ -91,7 +94,7 @@ describe("AiFillMarker", () => { item={{ id: "i1", ai_fill: fill({ status, stock_item_id: null }), pricing_source: "" }} />, ); - expect(screen.getByText("AI: no match")).toBeInTheDocument(); + expect(screen.getByRole("button", { name: "AI: no match" })).toBeInTheDocument(); }); it("lists candidates in the no_match popover", async () => { @@ -112,7 +115,7 @@ describe("AiFillMarker", () => { }} />, ); - fireEvent.click(screen.getByText("AI: no match")); + fireEvent.click(screen.getByRole("button", { name: "AI: no match" })); expect(await screen.findByText("None were the right size")).toBeInTheDocument(); expect(screen.getByText("M12 Bolt")).toBeInTheDocument(); expect(screen.getByText("M8 Bolt")).toBeInTheDocument(); diff --git a/src/components/ai-fill-marker.tsx b/src/components/ai-fill-marker.tsx index 6916b9e..0a2b76c 100644 --- a/src/components/ai-fill-marker.tsx +++ b/src/components/ai-fill-marker.tsx @@ -3,7 +3,7 @@ import Link from "next/link"; import { Bot } from "lucide-react"; -import { Badge } from "@/components/ui/badge"; +import { Badge, badgeVariants } from "@/components/ui/badge"; import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; import { isAiFillPending } from "@/lib/quote-assistant-review"; import type { QuoteItem, QuoteItemAiFill } from "@/lib/types"; @@ -65,9 +65,9 @@ function FilledNotes({ fill }: { fill: QuoteItemAiFill }) { {fill.reason ?

{fill.reason}

: null} {fill.doubts.length > 0 ? ( -
- Doubts -
    +
    + Doubts +
      {fill.doubts.map((d, i) => (
    • {d}
    • ))} @@ -121,12 +121,11 @@ export function AiFillMarker({ return ( - } + className={cn( + badgeVariants({ variant: "secondary" }), + "cursor-pointer text-muted-foreground outline-none focus-visible:ring-[3px] focus-visible:ring-ring/40", + className, + )} > AI: no match diff --git a/src/components/ai-fill-quote-button.test.tsx b/src/components/ai-fill-quote-button.test.tsx index 7a41c1f..3350f33 100644 --- a/src/components/ai-fill-quote-button.test.tsx +++ b/src/components/ai-fill-quote-button.test.tsx @@ -43,6 +43,33 @@ describe("AiFillQuoteButton", () => { expect(toast.success).toHaveBeenCalledWith("AI filled 3 lines ยท 2 without a match"); }); + it("uses the singular for one line and omits the unmatched suffix", async () => { + fetchMock.mockReturnValue(json(200, { filled: 1, unmatched: 0 })); + render( {}} />); + fireEvent.click(screen.getByRole("button")); + await waitFor(() => expect(toast.success).toHaveBeenCalledWith("AI filled 1 line")); + }); + + it("shows an info toast when there was nothing to fill", async () => { + fetchMock.mockReturnValue(json(200, { filled: 0, unmatched: 0 })); + const onDone = vi.fn(); + render(); + fireEvent.click(screen.getByRole("button")); + await waitFor(() => expect(toast.info).toHaveBeenCalledWith("AI had no open lines to fill")); + expect(toast.success).not.toHaveBeenCalled(); + expect(onDone).toHaveBeenCalledTimes(1); + }); + + it("toasts the error on a 200 that carries an error, and still refreshes", async () => { + fetchMock.mockReturnValue(json(200, { filled: 0, unmatched: 0, error: "AI is switched off" })); + const onDone = vi.fn(); + render(); + fireEvent.click(screen.getByRole("button")); + await waitFor(() => expect(toast.error).toHaveBeenCalledWith("AI is switched off")); + expect(toast.success).not.toHaveBeenCalled(); + expect(onDone).toHaveBeenCalledTimes(1); + }); + it("toasts the error text on a 409", async () => { fetchMock.mockReturnValue(json(409, { error: "AI is already working on this quote" })); const onDone = vi.fn();