test(context): OrderItemContextPanel UI tests

Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
SJCoetz 2026-08-07 02:54:24 +02:00
parent efd2ad6540
commit b3a73c0040

View file

@ -0,0 +1,107 @@
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { cleanup, fireEvent, render, screen, waitFor } from "@testing-library/react";
import { OrderItemContextPanel } from "@/components/order-item-context-panel";
import type { OrderItemContextResponse } from "@/lib/server/order-item-context-service";
afterEach(cleanup);
const fullContext: OrderItemContextResponse = {
generated_at: "2026-08-07T10:00:00Z",
match: { sku: "DRL-6MM", match_kind: "sku_exact", match_confidence: 1 },
last_quote: {
quote_item_id: "qi1", quote_id: "q1", quote_number: "Q-26080701",
customer_name: "Acme", company: "Acme Ltd", status: "approved",
cost_price: 100, selling_price: 150, markup_percent: 50, days_ago: 5,
},
last_order: {
order_item_id: "oi1", order_id: "o1", order_number: "ORD-26080702",
company: "Acme Ltd", cost_price: 110, selling_price: 150,
markup_percent: 36.4, item_status: "delivered", updated_at: "2026-08-01T00:00:00Z", days_ago: 6,
},
last_invoiced: {
order_item_id: "oi2", order_id: "o2", order_number: "ORD-26080703",
supplier_invoice_number: "INV-99", customer_invoice_number: "C-7",
invoiced_date: "2026-08-02T00:00:00Z", cost_price: 105, quantity: 10, qty_invoiced: 10, days_ago: 5,
},
stock: {
stock_item_id: "s1", name: "Drill 6mm", sku: "DRL-6MM",
current_qty: 240, min_qty: 20, selling_price: 140, markup_percent: 40, status: "in_stock",
},
suppliers: [],
cheapest: null,
};
describe("OrderItemContextPanel", () => {
beforeEach(() => {
vi.restoreAllMocks();
});
it("renders all 4 rows when context is fully populated", async () => {
vi.spyOn(globalThis, "fetch").mockResolvedValue({
ok: true,
status: 200,
json: async () => fullContext,
} as Response);
render(<OrderItemContextPanel itemId="x" onApply={() => {}} />);
await waitFor(() => {
expect(screen.getByText(/Last quote/)).toBeInTheDocument();
expect(screen.getByText(/Last order/)).toBeInTheDocument();
expect(screen.getByText(/Last invoice/)).toBeInTheDocument();
expect(screen.getByText(/Stock/)).toBeInTheDocument();
});
});
it("renders the dashed empty-state card when context is all-null", async () => {
const empty: OrderItemContextResponse = {
...fullContext,
last_quote: null, last_order: null, last_invoiced: null, stock: null, suppliers: [],
};
vi.spyOn(globalThis, "fetch").mockResolvedValue({
ok: true,
status: 200,
json: async () => empty,
} as Response);
render(<OrderItemContextPanel itemId="x" onApply={() => {}} />);
await waitFor(() => {
expect(screen.getByText(/No context for this item yet/)).toBeInTheDocument();
});
});
it("fires onApply with the row's cost and source when Apply is clicked", async () => {
vi.spyOn(globalThis, "fetch").mockResolvedValue({
ok: true,
status: 200,
json: async () => fullContext,
} as Response);
const onApply = vi.fn();
render(<OrderItemContextPanel itemId="x" onApply={onApply} />);
await waitFor(() => screen.getByText(/Last quote/));
// Walk up two levels: span (Last quote) → label div → row div (which contains the Apply button).
const quoteRow = screen.getByText(/Last quote/).parentElement!.parentElement!;
const applyBtn = quoteRow.querySelector("button")!;
fireEvent.click(applyBtn);
expect(onApply).toHaveBeenCalledWith(expect.objectContaining({
source: "last_quote",
cost_price: 100,
source_label: "Q-26080701",
}));
});
it("marks the invoice row's selling cell as dimmed", async () => {
vi.spyOn(globalThis, "fetch").mockResolvedValue({
ok: true,
status: 200,
json: async () => fullContext,
} as Response);
render(<OrderItemContextPanel itemId="x" onApply={() => {}} />);
await waitFor(() => screen.getByText(/Last invoice/));
// The dimmed class is applied to the selling cell on the invoice row.
const dimmed = document.querySelector(".text-muted-foreground\\/50");
expect(dimmed).not.toBeNull();
});
});