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 (
);
}
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);
});
});