From 4cf9c782e6d1229eb3ea1c87ca599d7b17f4ca6e Mon Sep 17 00:00:00 2001 From: SJCoetz Date: Thu, 30 Jul 2026 23:33:12 +0200 Subject: [PATCH] feat(stock-quick-draft): mount modal on /stock-orders via New Stock Item button Co-authored-by: Orca --- src/app/(app)/stock-orders/page.test.tsx | 138 +++++++++++++++++++++++ src/app/(app)/stock-orders/page.tsx | 15 +++ 2 files changed, 153 insertions(+) create mode 100644 src/app/(app)/stock-orders/page.test.tsx diff --git a/src/app/(app)/stock-orders/page.test.tsx b/src/app/(app)/stock-orders/page.test.tsx new file mode 100644 index 0000000..3582438 --- /dev/null +++ b/src/app/(app)/stock-orders/page.test.tsx @@ -0,0 +1,138 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; +import { cleanup, render, screen, fireEvent } from "@testing-library/react"; + +// ── Mocks ──────────────────────────────────────────────────────────────── +// +// The page is a client component that reads PocketBase via +// `@/lib/pocketbase`. We mock both the `pocketbase` npm package +// (to keep EventSource out of jsdom) and the local helper module. + +// Shared hoisted state so both mocks agree on what getList returns. +const { _state, _collectionMock, _resetState } = vi.hoisted(() => { + const state = { + items: [] as unknown[], + error: undefined as string | undefined, + }; + const collectionMock = { + getList: vi.fn().mockImplementation(async () => ({ + items: state.items, + totalItems: state.items.length, + page: 1, + perPage: 50, + totalPages: 1, + })), + getFullList: vi.fn().mockResolvedValue([]), + }; + return { + _state: state, + _collectionMock: collectionMock, + _resetState: () => { + state.items.length = 0; + state.error = undefined; + collectionMock.getList.mockClear(); + collectionMock.getList.mockImplementation(async () => ({ + items: state.items, + totalItems: state.items.length, + page: 1, + perPage: 50, + totalPages: 1, + })); + }, + }; +}); + +vi.mock("pocketbase", () => ({ + default: class MockPocketBase { + autoCancellation() {} + collection(_name: string) { + return _collectionMock; + } + }, +})); + +vi.mock("@/lib/pocketbase", () => ({ + __esModule: true, + default: () => ({ + collection: () => _collectionMock, + }), + getPocketBase: () => ({ + collection: () => _collectionMock, + }), +})); + +vi.mock("next/navigation", () => ({ + useRouter: () => ({ + push: vi.fn(), + replace: vi.fn(), + back: vi.fn(), + }), +})); + +// Stub the QuickDraftModal — it's already tested in +// `src/components/stock-orders/quick-draft-modal.test.tsx`. This test +// only cares that the page mounts the modal and toggles its `open` +// prop when the button is clicked. +vi.mock("@/components/stock-orders/quick-draft-modal", () => ({ + QuickDraftModal: ({ + open, + onOpenChange, + }: { + open: boolean; + onOpenChange: (b: boolean) => void; + }) => ( +
+ +
+ ), +})); + +import StockOrdersPage from "./page"; + +// ── Tests ──────────────────────────────────────────────────────────────── + +describe("StockOrdersPage — quick draft button", () => { + beforeEach(() => { + _resetState(); + }); + + afterEach(() => { + cleanup(); + }); + + it("renders a New Stock Item button in the page header", async () => { + render(); + const button = await screen.findByTestId("new-stock-item-button"); + expect(button).toBeInTheDocument(); + expect(button).toHaveTextContent(/new stock item/i); + }); + + it("mounts the QuickDraftModal (closed by default)", async () => { + render(); + const modal = await screen.findByTestId("quick-draft-modal"); + expect(modal).toBeInTheDocument(); + expect(modal.dataset.open).toBe("false"); + }); + + it("opens the QuickDraftModal when the button is clicked", async () => { + render(); + const modal = await screen.findByTestId("quick-draft-modal"); + expect(modal.dataset.open).toBe("false"); + fireEvent.click(screen.getByTestId("new-stock-item-button")); + expect(modal.dataset.open).toBe("true"); + }); + + it("closes the QuickDraftModal when the modal calls onOpenChange(false)", async () => { + render(); + const modal = await screen.findByTestId("quick-draft-modal"); + fireEvent.click(screen.getByTestId("new-stock-item-button")); + expect(modal.dataset.open).toBe("true"); + fireEvent.click(screen.getByTestId("quick-draft-modal-close")); + expect(modal.dataset.open).toBe("false"); + }); +}); \ No newline at end of file diff --git a/src/app/(app)/stock-orders/page.tsx b/src/app/(app)/stock-orders/page.tsx index 240fde4..a1cb840 100644 --- a/src/app/(app)/stock-orders/page.tsx +++ b/src/app/(app)/stock-orders/page.tsx @@ -16,6 +16,7 @@ import { Card, CardContent } from "@/components/ui/card"; import { Loader2, Plus, AlertCircle, Package, ChevronRight, Boxes } from "lucide-react"; import { toast } from "sonner"; import getPocketBase from "@/lib/pocketbase"; +import { QuickDraftModal } from "@/components/stock-orders/quick-draft-modal"; import { STOCK_PURCHASE_STATUS_VALUES, type StockPurchase, @@ -93,6 +94,7 @@ export default function StockOrdersPage() { const [status, setStatus] = useState(""); const [page, setPage] = useState(1); const [totalPages, setTotalPages] = useState(1); + const [quickDraftOpen, setQuickDraftOpen] = useState(false); const fetchRows = useCallback(async () => { setLoading(true); @@ -160,6 +162,14 @@ export default function StockOrdersPage() { Reorder from low stock + )} + + ); }