import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; import { render, screen, fireEvent, cleanup } from "@testing-library/react"; const { pushMock } = vi.hoisted(() => ({ pushMock: vi.fn() })); vi.mock("next/navigation", () => ({ useRouter: () => ({ push: pushMock }) })); import { UnsavedChangesGuard } from "@/components/po/unsaved-changes-guard"; // Each test adds a real to the document so the capture-phase click // interceptor has something to catch. const links: HTMLAnchorElement[] = []; function addLink(href: string) { const a = document.createElement("a"); a.setAttribute("href", href); a.textContent = "go"; document.body.appendChild(a); links.push(a); return a; } beforeEach(() => pushMock.mockClear()); afterEach(() => { cleanup(); links.splice(0).forEach((a) => a.remove()); }); const noop = () => {}; describe("UnsavedChangesGuard", () => { it("does not intercept navigation when there are no unsaved changes", () => { render(); const link = addLink("/catalogue/vendors"); const notCanceled = fireEvent.click(link); expect(notCanceled).toBe(true); // default not prevented → navigation proceeds expect(screen.queryByText("Unsaved changes")).not.toBeInTheDocument(); }); it("intercepts an internal link click and opens the prompt when dirty", () => { render(); const link = addLink("/catalogue/vendors"); const notCanceled = fireEvent.click(link); expect(notCanceled).toBe(false); // navigation blocked expect(pushMock).not.toHaveBeenCalled(); expect(screen.getByText("Unsaved changes")).toBeInTheDocument(); expect(screen.getByRole("button", { name: "Save as draft" })).toBeInTheDocument(); expect(screen.getByRole("button", { name: "Discard changes" })).toBeInTheDocument(); expect(screen.getByRole("button", { name: "Stay on page" })).toBeInTheDocument(); }); it("does not intercept external links (left to the browser's own prompt)", () => { render(); const link = addLink("https://example.com/elsewhere"); const notCanceled = fireEvent.click(link); expect(notCanceled).toBe(true); expect(screen.queryByText("Unsaved changes")).not.toBeInTheDocument(); }); it("'Stay on page' closes the prompt without navigating", () => { render(); fireEvent.click(addLink("/dashboard")); fireEvent.click(screen.getByRole("button", { name: "Stay on page" })); expect(screen.queryByText("Unsaved changes")).not.toBeInTheDocument(); expect(pushMock).not.toHaveBeenCalled(); }); it("'Discard changes' navigates to the intended destination", () => { render(); fireEvent.click(addLink("/catalogue/vendors")); fireEvent.click(screen.getByRole("button", { name: "Discard changes" })); expect(pushMock).toHaveBeenCalledWith("/catalogue/vendors"); }); it("'Save as draft' runs the save handler and closes the prompt", () => { const onSaveDraft = vi.fn(); render(); fireEvent.click(addLink("/dashboard")); fireEvent.click(screen.getByRole("button", { name: "Save as draft" })); expect(onSaveDraft).toHaveBeenCalledTimes(1); expect(pushMock).not.toHaveBeenCalled(); // the save action does its own redirect expect(screen.queryByText("Unsaved changes")).not.toBeInTheDocument(); }); it("arms the browser beforeunload prompt only while dirty", () => { const { rerender } = render(); const dirtyEvt = new Event("beforeunload", { cancelable: true }); window.dispatchEvent(dirtyEvt); expect(dirtyEvt.defaultPrevented).toBe(true); rerender(); const cleanEvt = new Event("beforeunload", { cancelable: true }); window.dispatchEvent(cleanEvt); expect(cleanEvt.defaultPrevented).toBe(false); }); });