fix(quote-assistant): warn-coloured doubts, real button for no-match badge, more button tests

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Dave 2026-10-16 19:48:19 +02:00
parent ef6193367d
commit c8e977c61e
3 changed files with 41 additions and 12 deletions

View file

@ -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();

View file

@ -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 ? <p className="text-xs">{fill.reason}</p> : null}
{fill.doubts.length > 0 ? (
<div className="flex flex-col gap-1">
<span className="text-xs font-medium">Doubts</span>
<ul className="list-disc pl-4 text-xs text-muted-foreground">
<div className="flex flex-col gap-1 rounded-md border border-warn/30 bg-warn/10 px-2 py-1.5">
<span className="text-xs font-medium text-warn">Doubts</span>
<ul className="list-disc pl-4 text-xs text-warn">
{fill.doubts.map((d, i) => (
<li key={`${i}-${d}`}>{d}</li>
))}
@ -121,12 +121,11 @@ export function AiFillMarker({
return (
<Popover>
<PopoverTrigger
render={
<Badge
variant="secondary"
className={cn("cursor-pointer text-muted-foreground", className)}
/>
}
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
</PopoverTrigger>

View file

@ -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(<AiFillQuoteButton quoteId="q1" onDone={() => {}} />);
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(<AiFillQuoteButton quoteId="q1" onDone={onDone} />);
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(<AiFillQuoteButton quoteId="q1" onDone={onDone} />);
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();