import { describe, it, expect, vi, beforeEach } from "vitest"; import { render, screen, fireEvent, waitFor } from "@testing-library/react"; import { UnsavedChangesDialog } from "@/components/po/unsaved-changes-dialog"; import { useUnsavedChanges } from "@/components/po/use-unsaved-changes"; const push = vi.fn(); vi.mock("next/navigation", () => ({ useRouter: () => ({ push, back: vi.fn() }), })); beforeEach(() => { push.mockReset(); }); // ── Dialog ──────────────────────────────────────────────────────────────────── describe("UnsavedChangesDialog", () => { const noop = async () => ({ ok: true }); it("renders the three choices when open", () => { render( ); expect(screen.getByRole("button", { name: /save as draft/i })).toBeInTheDocument(); expect(screen.getByRole("button", { name: /discard changes/i })).toBeInTheDocument(); expect(screen.getByRole("button", { name: /stay on page/i })).toBeInTheDocument(); }); it("renders nothing when closed", () => { const { container } = render( ); expect(container).toBeEmptyDOMElement(); }); it("invokes onDiscard / onStay when those buttons are clicked", () => { const onDiscard = vi.fn(); const onStay = vi.fn(); render( ); fireEvent.click(screen.getByRole("button", { name: /discard changes/i })); expect(onDiscard).toHaveBeenCalledOnce(); fireEvent.click(screen.getByRole("button", { name: /stay on page/i })); expect(onStay).toHaveBeenCalledOnce(); }); it("shows an error and keeps the prompt open when saving the draft fails", async () => { const onSaveDraft = vi.fn(async () => ({ ok: false, error: "Title is required" })); const onStay = vi.fn(); render( ); fireEvent.click(screen.getByRole("button", { name: /save as draft/i })); expect(onSaveDraft).toHaveBeenCalledOnce(); await waitFor(() => expect(screen.getByText("Title is required")).toBeInTheDocument()); // still open — user can recover expect(onStay).not.toHaveBeenCalled(); expect(screen.getByRole("button", { name: /stay on page/i })).toBeInTheDocument(); }); it("uses a custom save label when provided", () => { render( ); expect(screen.getByRole("button", { name: "Save Draft" })).toBeInTheDocument(); }); }); // ── Hook ──────────────────────────────────────────────────────────────────── function Harness({ dirty }: { dirty: boolean }) { const { promptOpen, guard, leave, stay } = useUnsavedChanges(dirty); return (
{String(promptOpen)} internal link same page anchor external link
); } describe("useUnsavedChanges", () => { it("intercepts internal link clicks while dirty", () => { render(); expect(screen.getByTestId("open")).toHaveTextContent("false"); fireEvent.click(screen.getByText("internal link")); expect(screen.getByTestId("open")).toHaveTextContent("true"); expect(push).not.toHaveBeenCalled(); }); it("does not intercept link clicks when clean", () => { render(); fireEvent.click(screen.getByText("internal link")); expect(screen.getByTestId("open")).toHaveTextContent("false"); }); it("ignores external links", () => { render(); fireEvent.click(screen.getByText("external link")); expect(screen.getByTestId("open")).toHaveTextContent("false"); }); it("intercepts guarded programmatic navigation while dirty, then proceeds on leave", () => { render(); fireEvent.click(screen.getByText("cancel")); expect(screen.getByTestId("open")).toHaveTextContent("true"); expect(push).not.toHaveBeenCalled(); fireEvent.click(screen.getByText("leave")); expect(push).toHaveBeenCalledWith("/programmatic"); expect(screen.getByTestId("open")).toHaveTextContent("false"); }); it("runs programmatic navigation immediately when clean", () => { render(); fireEvent.click(screen.getByText("cancel")); expect(push).toHaveBeenCalledWith("/programmatic"); expect(screen.getByTestId("open")).toHaveTextContent("false"); }); it("closes the prompt without navigating on stay", () => { render(); fireEvent.click(screen.getByText("cancel")); expect(screen.getByTestId("open")).toHaveTextContent("true"); fireEvent.click(screen.getByText("stay")); expect(screen.getByTestId("open")).toHaveTextContent("false"); expect(push).not.toHaveBeenCalled(); }); it("warns via beforeunload while dirty", () => { render(); const evt = new Event("beforeunload", { cancelable: true }); window.dispatchEvent(evt); expect(evt.defaultPrevented).toBe(true); }); it("does not warn via beforeunload when clean", () => { render(); const evt = new Event("beforeunload", { cancelable: true }); window.dispatchEvent(evt); expect(evt.defaultPrevented).toBe(false); }); });