diff --git a/src/app/(app)/stock-orders/StockOrdersTable.test.tsx b/src/app/(app)/stock-orders/StockOrdersTable.test.tsx
new file mode 100644
index 0000000..ead17f7
--- /dev/null
+++ b/src/app/(app)/stock-orders/StockOrdersTable.test.tsx
@@ -0,0 +1,209 @@
+import { describe, it, expect, vi, afterEach } from "vitest";
+import { cleanup, render, screen, fireEvent } from "@testing-library/react";
+
+// Stub the QuickDraftModal — it's tested in
+// `src/components/stock-orders/quick-draft-modal.test.tsx`.
+vi.mock("@/components/stock-orders/quick-draft-modal", () => ({
+ QuickDraftModal: ({
+ open,
+ onOpenChange,
+ editPurchaseId,
+ }: {
+ open: boolean;
+ onOpenChange: (b: boolean) => void;
+ editPurchaseId?: string;
+ }) => (
+
+
+
+ ),
+}));
+
+// Stub Next's so the test doesn't pull the router — these
+// tests don't assert navigation.
+vi.mock("next/link", () => ({
+ default: ({
+ children,
+ href,
+ ...rest
+ }: {
+ children: React.ReactNode;
+ href: string;
+ [k: string]: unknown;
+ }) => (
+
+ {children}
+
+ ),
+}));
+
+// Stub Next's `useRouter` — the table rows are clickable and push
+// `/stock-orders/[id]`, which jsdom can't handle.
+vi.mock("next/navigation", () => ({
+ useRouter: () => ({
+ push: vi.fn(),
+ replace: vi.fn(),
+ back: vi.fn(),
+ }),
+}));
+
+import StockOrdersTable from "./StockOrdersTable";
+import type { StockPurchase, Supplier } from "@/lib/types";
+
+// --- Helpers --------------------------------------------------------------
+
+// Inline shape (the plan's fallback) — `Parameters`
+// is too tight because the `StockPurchase` type requires every `Supplier`
+// field expanded (email, address, etc.), but the table only reads
+// `expand.supplier.suppliername`. The component's prop type itself is
+// fine; this is just a fixture-side cast.
+type ListRow = StockPurchase & {
+ item_count: number;
+ ordered_value: number;
+ received_value: number;
+ expand?: { supplier?: Supplier };
+};
+
+function makeRow(overrides: {
+ id: string;
+ status: "draft" | "sent" | "received" | "cancelled" | "sending" | "mail_failed";
+ purchase_number: string;
+}): ListRow {
+ return {
+ id: overrides.id,
+ created: "2026-07-30 10:00:00.000Z",
+ updated: "2026-07-30 10:00:00.000Z",
+ collectionId: "stock_purchases",
+ collectionName: "stock_purchases",
+ purchase_number: overrides.purchase_number,
+ supplier: "supplier-1",
+ status: overrides.status,
+ notes: "",
+ email_error: "",
+ email_message_id: "",
+ item_count: 0,
+ ordered_value: 0,
+ received_value: 0,
+ expand: {
+ supplier: {
+ id: "supplier-1",
+ collectionId: "suppliers",
+ collectionName: "suppliers",
+ created: "",
+ updated: "",
+ suppliername: "Steel & Pipe Co",
+ suppliernumber: "SUP-1",
+ supplieraddress: "",
+ email: "",
+ supply_tags: "",
+ coordinates: "",
+ closingtime: "",
+ } as Supplier,
+ },
+ };
+}
+
+// --- Tests ----------------------------------------------------------------
+
+describe("StockOrdersTable — quick draft button", () => {
+ afterEach(() => {
+ cleanup();
+ });
+
+ it("renders a New Stock Item button", () => {
+ render();
+ const button = screen.getByTestId("new-stock-item-button");
+ expect(button).toBeInTheDocument();
+ expect(button).toHaveTextContent(/new stock item/i);
+ });
+
+ it("mounts the QuickDraftModal (closed by default)", () => {
+ render();
+ const modal = screen.getByTestId("quick-draft-modal");
+ expect(modal).toBeInTheDocument();
+ expect(modal.dataset.open).toBe("false");
+ });
+
+ it("opens the QuickDraftModal when the button is clicked", () => {
+ render();
+ fireEvent.click(screen.getByTestId("new-stock-item-button"));
+ expect(screen.getByTestId("quick-draft-modal").dataset.open).toBe("true");
+ });
+
+ it("closes the QuickDraftModal when the modal calls onOpenChange(false)", () => {
+ render();
+ fireEvent.click(screen.getByTestId("new-stock-item-button"));
+ expect(screen.getByTestId("quick-draft-modal").dataset.open).toBe("true");
+ fireEvent.click(screen.getByTestId("quick-draft-modal-close"));
+ expect(screen.getByTestId("quick-draft-modal").dataset.open).toBe("false");
+ });
+});
+
+describe("StockOrdersTable — edit draft button", () => {
+ afterEach(() => {
+ cleanup();
+ });
+
+ it("renders an Edit button on draft rows", () => {
+ const rows = [
+ makeRow({ id: "po-abc", status: "draft", purchase_number: "PO26000001STK" }),
+ ];
+ render();
+ const editButton = screen.getByTestId("edit-draft-button");
+ expect(editButton).toBeInTheDocument();
+ expect(editButton).toHaveAttribute("aria-label", "Edit draft PO26000001STK");
+ });
+
+ it("does NOT render an Edit button on non-draft rows", () => {
+ const rows = [
+ makeRow({ id: "po-sent", status: "sent", purchase_number: "PO26000002STK" }),
+ ];
+ render();
+ expect(screen.queryByTestId("edit-draft-button")).toBeNull();
+ });
+
+ it("clicking Edit opens the modal with editPurchaseId set to the row id", () => {
+ const rows = [
+ makeRow({ id: "po-abc", status: "draft", purchase_number: "PO26000001STK" }),
+ ];
+ render();
+ const modal = screen.getByTestId("quick-draft-modal");
+ expect(modal.dataset.open).toBe("false");
+ expect(modal.dataset.editPurchaseId).toBe("");
+ fireEvent.click(screen.getByTestId("edit-draft-button"));
+ expect(modal.dataset.open).toBe("true");
+ expect(modal.dataset.editPurchaseId).toBe("po-abc");
+ });
+});
+
+describe("StockOrdersTable — total value columns", () => {
+ afterEach(() => {
+ cleanup();
+ });
+
+ it("renders ordered_value and received_value formatted as ZAR currency", () => {
+ const row = makeRow({
+ id: "po-1",
+ status: "draft",
+ purchase_number: "PO26000001STK",
+ });
+ row.ordered_value = 521;
+ row.received_value = 75;
+ render();
+ // formatCurrency uses Intl with en-ZA / ZAR — the exact glyph
+ // and spacing can vary by ICU version, so assert on the numeric
+ // parts instead.
+ expect(screen.getAllByText(/521/).length).toBeGreaterThan(0);
+ expect(screen.getAllByText(/75/).length).toBeGreaterThan(0);
+ });
+});