WFDASH-WEBAPP/src/components/collection-confirm/items-table-ai.test.tsx
Dave 8efee9db42 fix(collection-ai): pre-fill only from the chip's tax invoice, AI column only with ai_check
M2: the confirm dialog pre-fill uses the card's newest document (as the
chip) and only when it is a tax invoice.
M3: the AI ticks column (and its min-width) shows only when a row has
ai_check; the POS column stays whenever in_pos is known.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 10:20:46 +02:00

157 lines
6.9 KiB
TypeScript

import { render, screen, within } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { describe, expect, it, vi } from "vitest";
import type { ConfirmRow } from "@/lib/server/collection-confirm-service";
import { makeAiRow, makeCheck } from "./ai-fixtures";
import { ItemsTable } from "./items-table";
const plain: ConfirmRow = {
id: "row-p",
sku: "SK-9",
description: "Plain",
order_qty: 1,
qty_collected: 1,
cost_price: 1,
surplus_qty: 0,
};
const noop = vi.fn();
describe("ItemsTable AI layer", () => {
it("renders the original six headers and no AI/tick/robot UI without AI data", () => {
render(<ItemsTable rows={[plain]} onCostChange={noop} onSkuChange={noop} />);
const headers = screen.getAllByRole("columnheader").map((h) => h.textContent);
expect(headers).toEqual(["SKU", "Description", "Ord", "Coll", "Cost", "Surplus"]);
expect(screen.queryByLabelText(/^SKU: /)).not.toBeInTheDocument();
expect(screen.queryByTestId("ai-sku-robot-row-p")).not.toBeInTheDocument();
expect(screen.queryByText(/add to pos/i)).not.toBeInTheDocument();
});
it("shows only the POS column (no AI column, no AI min-width) when no row has ai_check", () => {
const { container } = render(
<ItemsTable rows={[{ ...plain, in_pos: false }]} onCostChange={noop} onSkuChange={noop} onAddToPos={noop} />,
);
const headers = screen.getAllByRole("columnheader").map((h) => h.textContent);
expect(headers).toEqual(["SKU", "Description", "Ord", "Coll", "Cost", "Surplus", "POS"]);
expect(container.querySelector("table")?.className).not.toContain("min-w-[860px]");
expect(container.querySelectorAll("col")).toHaveLength(7);
expect(screen.getByText(/tap the pencil/i).closest("td")).toHaveAttribute("colspan", "7");
});
it("adds AI and POS headers when a row has ai_check", () => {
render(<ItemsTable rows={[makeAiRow()]} onCostChange={noop} onSkuChange={noop} />);
const headers = screen.getAllByRole("columnheader").map((h) => h.textContent);
expect(headers).toEqual(["SKU", "Description", "Ord", "Coll", "Cost", "Surplus", "AI", "POS"]);
});
it("renders four ticks with aria-labels from computeLineTicks and ours-vs-document titles", () => {
const row = makeAiRow({
sku: "SK-001",
qty_collected: 9,
cost_price: 45,
ai_check: makeCheck({
same_item: "likely",
doc_line: {
line_no: 1,
code: "SK-001",
description: "Totally different thing",
quantity: 10,
unit: "ea",
unit_price: null,
line_total: null,
to_follow: null,
price_note: "",
},
}),
});
render(<ItemsTable rows={[row]} onCostChange={noop} onSkuChange={noop} />);
expect(screen.getByLabelText(/^SKU: matches/)).toBeInTheDocument();
expect(screen.getByLabelText(/^Qty: doesn't match/)).toBeInTheDocument();
expect(screen.getByLabelText(/^Desc: have a look/)).toBeInTheDocument();
expect(screen.getByLabelText(/^Price: can't check/)).toBeInTheDocument();
expect(screen.getByLabelText(/^Qty: doesn't match/)).toHaveAttribute(
"title",
expect.stringContaining("9"),
);
expect(screen.getByLabelText(/^Price: can't check — Ours: R45.00/)).toBeInTheDocument();
const desc = screen.getByLabelText(/^Desc: have a look/).getAttribute("title") ?? "";
expect(desc).toContain("Item 1");
expect(desc).toContain("Totally different thing");
});
it("a 'different' verdict is all red even when the code matches", () => {
const row = makeAiRow({ ai_check: makeCheck({ same_item: "different" }) });
render(<ItemsTable rows={[row]} onCostChange={noop} onSkuChange={noop} />);
expect(screen.getByLabelText(/^SKU: doesn't match/)).toBeInTheDocument();
expect(screen.getByLabelText(/^Desc: doesn't match/)).toBeInTheDocument();
});
it("shows the amber label", () => {
const row = makeAiRow({ ai_check: makeCheck({ same_item: "likely" }) });
render(<ItemsTable rows={[row]} onCostChange={noop} onSkuChange={noop} />);
expect(screen.getByLabelText(/^Desc: have a look/)).toBeInTheDocument();
});
it("shows the robot next to the SKU only while the SKU equals sku_filled_value", () => {
const filled = makeAiRow({ id: "a", sku: "SK-001", ai_check: makeCheck({ sku_filled_value: "SK-001" }) });
const human = makeAiRow({ id: "b", sku: "HUMAN", ai_check: makeCheck({ sku_filled_value: "SK-001" }) });
render(<ItemsTable rows={[filled, human]} onCostChange={noop} onSkuChange={noop} />);
expect(screen.getByTestId("ai-sku-robot-a")).toBeInTheDocument();
expect(screen.queryByTestId("ai-sku-robot-b")).not.toBeInTheDocument();
});
it("stock rows: shows, ticks and robot-marks the line's own SKU (line_sku)", () => {
const stock = makeAiRow({
id: "s1",
source: "stock",
sku: undefined,
line_sku: "SK-001",
ai_check: makeCheck({ sku_filled_value: "SK-001" }),
});
render(<ItemsTable rows={[stock]} onCostChange={noop} onSkuChange={noop} />);
const rowEl = screen.getByTestId("ai-ticks-s1").closest("tr")!;
expect(within(rowEl).getByText("SK-001")).toBeInTheDocument();
expect(screen.getByTestId("ai-sku-robot-s1")).toBeInTheDocument();
expect(screen.getByLabelText(/^SKU: matches/)).toBeInTheDocument();
});
it("shows In POS with a stock link when in_pos", () => {
const row = makeAiRow({ in_pos: true, pos_stock_item_id: "stock-77" });
render(<ItemsTable rows={[row]} onCostChange={noop} onSkuChange={noop} onAddToPos={noop} />);
const link = screen.getByRole("link", { name: /in pos/i });
expect(link).toHaveAttribute("href", "/stock/stock-77");
expect(screen.queryByRole("button", { name: /add to pos/i })).not.toBeInTheDocument();
});
it("calls onAddToPos with the row id when not in POS", async () => {
const onAdd = vi.fn();
render(<ItemsTable rows={[makeAiRow()]} onCostChange={noop} onSkuChange={noop} onAddToPos={onAdd} />);
await userEvent.click(screen.getByRole("button", { name: /add to pos/i }));
expect(onAdd).toHaveBeenCalledWith("row-1");
});
it("disables Add to POS on a row without a SKU (Set a SKU first)", async () => {
const onAdd = vi.fn();
render(
<ItemsTable rows={[makeAiRow({ sku: "" })]} onCostChange={noop} onSkuChange={noop} onAddToPos={onAdd} />,
);
const button = screen.getByRole("button", { name: /add to pos/i });
expect(button).toBeDisabled();
expect(button).toHaveAttribute("title", "Set a SKU first");
await userEvent.click(button);
expect(onAdd).not.toHaveBeenCalled();
});
it("renders no tick icons for a row without ai_check inside an AI table", () => {
render(
<ItemsTable
rows={[makeAiRow(), { ...plain, in_pos: false }]}
onCostChange={noop}
onSkuChange={noop}
/>,
);
const rowEl = screen.getByText("Plain").closest("tr")!;
expect(within(rowEl).queryByLabelText(/^SKU: /)).not.toBeInTheDocument();
});
});