import { describe, it, expect } from "vitest"; import { getStockStatus, isLowStock, isLowStockItem, STOCK_STATUS_STYLES, type StockStatus, } from "./stock-status"; describe("getStockStatus", () => { it("returns 'ok' when current_qty is strictly above min_qty", () => { expect(getStockStatus(10, 5, true)).toBe("ok"); expect(getStockStatus(1, 0, true)).toBe("ok"); }); it("returns 'low' when current_qty equals min_qty", () => { expect(getStockStatus(5, 5, true)).toBe("low"); }); it("returns 'low' when current_qty is between 0 and min_qty (exclusive of 0)", () => { expect(getStockStatus(1, 5, true)).toBe("low"); expect(getStockStatus(3, 5, true)).toBe("low"); }); it("returns 'out' when current_qty is zero", () => { expect(getStockStatus(0, 5, true)).toBe("out"); expect(getStockStatus(0, 0, true)).toBe("out"); }); it("returns 'out' when current_qty is negative (treats as a bug, not a clamp)", () => { expect(getStockStatus(-1, 5, true)).toBe("out"); expect(getStockStatus(-100, 0, true)).toBe("out"); }); it("returns 'inactive' regardless of qty when is_active is false", () => { expect(getStockStatus(10, 5, false)).toBe("inactive"); expect(getStockStatus(0, 5, false)).toBe("inactive"); expect(getStockStatus(-1, 5, false)).toBe("inactive"); }); it("treats null/undefined qty as zero (so missing fields default to 'out')", () => { expect(getStockStatus(null, 5, true)).toBe("out"); expect(getStockStatus(undefined, 5, true)).toBe("out"); expect(getStockStatus(5, null, true)).toBe("ok"); }); it("coerces string-like numbers (PB sometimes returns numeric fields as strings)", () => { // `Number("5")` is 5, so this should work the same as the numeric path. expect(getStockStatus("5" as unknown as number, 5, true)).toBe("low"); expect(getStockStatus(3, "5" as unknown as number, true)).toBe("low"); }); }); describe("isLowStock", () => { it("includes 'low' AND 'out' but not 'ok' or 'inactive'", () => { expect(isLowStock(0, 5, true)).toBe(true); // out expect(isLowStock(3, 5, true)).toBe(true); // low expect(isLowStock(5, 5, true)).toBe(true); // low (boundary) expect(isLowStock(6, 5, true)).toBe(false); // ok expect(isLowStock(10, 5, false)).toBe(false); // inactive excluded }); }); describe("isLowStockItem", () => { it("delegates to isLowStock using the StockItem field shape", () => { expect( isLowStockItem({ current_qty: 0, min_qty: 5, is_active: true }), ).toBe(true); expect( isLowStockItem({ current_qty: 100, min_qty: 5, is_active: true }), ).toBe(false); // Inactive row with low stock — still excluded from the low-stock filter. expect( isLowStockItem({ current_qty: 0, min_qty: 5, is_active: false }), ).toBe(false); }); }); describe("STOCK_STATUS_STYLES", () => { it("exposes a labelled entry for every status", () => { const labels = Object.values(STOCK_STATUS_STYLES).map((s) => s.label); expect(labels).toContain("In stock"); expect(labels).toContain("Low stock"); expect(labels).toContain("Out of stock"); expect(labels).toContain("Inactive"); }); it("uses a red-leaning hex for 'out' so the list-page badge reads as urgent", () => { const outHex = STOCK_STATUS_STYLES.out.hex.toLowerCase(); // #b91c1c is red-700. We don't pin the exact value — only assert // the "red" intent: starts with #b and has a small green channel // (1c). Future palette tweaks should not break this test. expect(outHex.startsWith("#b")).toBe(true); }); }); /* ============================================================================ * Reorder-alert badge test * * Pinned as a separate describe block so the "is this row flagged as * needs-reordering" contract has a clearly-named home. A future * "this row needs to be re-ordered" UI cue (toast, banner, dashboard * widget) can target the same helper and add its own describe here. * ========================================================================== */ describe("reorder-alert badge logic", () => { it("flags 'low' as needing reorder (the operator's amber signal)", () => { const status = getStockStatus(3, 5, true); expect(status).toBe("low"); expect(STOCK_STATUS_STYLES[status].label).toBe("Low stock"); }); it("flags 'out' as needing reorder (the operator's red signal)", () => { const status = getStockStatus(0, 5, true); expect(status).toBe("out"); expect(STOCK_STATUS_STYLES[status].label).toBe("Out of stock"); }); it("does NOT flag 'ok' or 'inactive' as needing reorder", () => { expect(getStockStatus(100, 5, true)).toBe("ok"); expect(getStockStatus(100, 5, false)).toBe("inactive"); }); it("treats the boundary case (current == min) as 'low', not 'ok'", () => { // Boundary matters: a 5/5 item should already be in the // re-order queue, not at full health. expect(getStockStatus(5, 5, true)).toBe("low"); }); it("preserves the badge label across the list-page and detail-page call sites", () => { // Same helper, same palette — no per-page override that could // drift between the list and the detail view. const s1 = getStockStatus(2, 10, true); const s2 = getStockStatus(0, 10, true); expect(STOCK_STATUS_STYLES[s1].label).toBe("Low stock"); expect(STOCK_STATUS_STYLES[s2].label).toBe("Out of stock"); }); });