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:
parent
ef6193367d
commit
c8e977c61e
3 changed files with 41 additions and 12 deletions
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
Loading…
Reference in a new issue