import { renderHook, act } from "@testing-library/react"; import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; import type { CreateFlowState } from "../../../app/(app)/create/types"; import { useCreateFlowFinalize } from "../../../app/(app)/create/hooks/useCreateFlowFinalize"; import { publishRule, updatePublishedRule } from "../../../lib/create/api"; import { writeLastPublishedRule } from "../../../lib/create/lastPublishedRule"; import { CREATE_FLOW_PENDING_KEEP_RULE_LOGIN_KEY } from "../../../lib/create/pendingKeepRuleLogin"; import { CREATE_FLOW_COMPLETED_CELEBRATE_QUERY, CREATE_FLOW_COMPLETED_CELEBRATE_VALUE, } from "../../../app/(app)/create/utils/flowSteps"; vi.mock("../../../lib/create/buildPublishPayload", () => ({ buildPublishPayload: vi.fn(() => ({ ok: true as const, title: "Published title", summary: "Published summary", document: {}, })), })); vi.mock("../../../lib/create/api", () => ({ publishRule: vi.fn(), updatePublishedRule: vi.fn(), })); vi.mock("../../../lib/create/lastPublishedRule", () => ({ writeLastPublishedRule: vi.fn(), })); const emptyState = {} as CreateFlowState; const signedIn = { id: "user-1", email: "owner@example.com" }; describe("useCreateFlowFinalize", () => { const router = { push: vi.fn() }; const updateState = vi.fn(); const openLogin = vi.fn(); const onGuestPublished = vi.fn(); beforeEach(() => { vi.mocked(publishRule).mockReset(); vi.mocked(updatePublishedRule).mockReset(); vi.mocked(writeLastPublishedRule).mockReset(); router.push.mockReset(); updateState.mockReset(); openLogin.mockReset(); onGuestPublished.mockReset(); sessionStorage.clear(); }); afterEach(() => { vi.restoreAllMocks(); }); it("routes with celebrate query after initial POST publish", async () => { vi.mocked(publishRule).mockResolvedValue({ ok: true, id: "new-rule-id", title: "Published title", }); const { result } = renderHook(() => useCreateFlowFinalize({ state: emptyState, router, openLogin, updateState, loginReturnPath: "/create/final-review", sessionUser: signedIn, }), ); await act(async () => { await result.current.finalize(); }); expect(router.push).toHaveBeenCalledWith( `/create/completed?${CREATE_FLOW_COMPLETED_CELEBRATE_QUERY}=${CREATE_FLOW_COMPLETED_CELEBRATE_VALUE}`, ); expect(updatePublishedRule).not.toHaveBeenCalled(); expect(writeLastPublishedRule).toHaveBeenCalledWith({ id: "new-rule-id", title: "Published title", summary: "Published summary", document: {}, }); expect( sessionStorage.getItem(CREATE_FLOW_PENDING_KEEP_RULE_LOGIN_KEY), ).toBeNull(); }); it("does not publish while the session is unresolved", async () => { const { result } = renderHook(() => useCreateFlowFinalize({ state: emptyState, router, openLogin, updateState, loginReturnPath: "/create/final-review", sessionUser: undefined, }), ); await act(async () => { await result.current.finalize(); }); expect(publishRule).not.toHaveBeenCalled(); expect(router.push).not.toHaveBeenCalled(); }); it("omits stakeholder emails when a guest publishes with invites in state", async () => { vi.mocked(publishRule).mockResolvedValue({ ok: true, id: "guest-rule-id", title: "Published title", }); const { result } = renderHook(() => useCreateFlowFinalize({ state: { ...emptyState, stakeholderEmails: ["invitee@example.com"], }, router, openLogin, updateState, loginReturnPath: "/create/final-review", sessionUser: null, onGuestPublished, }), ); await act(async () => { await result.current.finalize(); }); expect(publishRule).toHaveBeenCalledWith({ title: "Published title", summary: "Published summary", document: {}, }); expect(onGuestPublished).toHaveBeenCalledWith({ skippedInvites: true }); expect(openLogin).not.toHaveBeenCalled(); expect(sessionStorage.getItem(CREATE_FLOW_PENDING_KEEP_RULE_LOGIN_KEY)).toBe("1"); expect(router.push).toHaveBeenCalledWith( `/create/completed?${CREATE_FLOW_COMPLETED_CELEBRATE_QUERY}=${CREATE_FLOW_COMPLETED_CELEBRATE_VALUE}`, ); }); it("hints to claim later when a guest publishes without stakeholder emails", async () => { vi.mocked(publishRule).mockResolvedValue({ ok: true, id: "guest-rule-id", title: "Published title", }); const { result } = renderHook(() => useCreateFlowFinalize({ state: emptyState, router, openLogin, updateState, loginReturnPath: "/create/final-review", sessionUser: null, onGuestPublished, }), ); await act(async () => { await result.current.finalize(); }); expect(publishRule).toHaveBeenCalledWith({ title: "Published title", summary: "Published summary", document: {}, }); expect(onGuestPublished).toHaveBeenCalledWith({ skippedInvites: false }); expect(openLogin).not.toHaveBeenCalled(); expect(sessionStorage.getItem(CREATE_FLOW_PENDING_KEEP_RULE_LOGIN_KEY)).toBe("1"); expect(router.push).toHaveBeenCalledWith( `/create/completed?${CREATE_FLOW_COMPLETED_CELEBRATE_QUERY}=${CREATE_FLOW_COMPLETED_CELEBRATE_VALUE}`, ); }); it("passes stakeholderEmails to publishRule on initial publish", async () => { vi.mocked(publishRule).mockResolvedValue({ ok: true, id: "new-rule-id", title: "Published title", }); const { result } = renderHook(() => useCreateFlowFinalize({ state: { ...emptyState, stakeholderEmails: ["invitee@example.com"], }, router, openLogin, updateState, loginReturnPath: "/create/final-review", sessionUser: signedIn, }), ); await act(async () => { await result.current.finalize(); }); expect(publishRule).toHaveBeenCalledWith( expect.objectContaining({ stakeholderEmails: ["invitee@example.com"], }), ); }); it("routes to /create/completed without celebrate after PATCH update", async () => { vi.mocked(updatePublishedRule).mockResolvedValue({ ok: true }); const { result } = renderHook(() => useCreateFlowFinalize({ state: { ...emptyState, editingPublishedRuleId: " existing-id ", }, router, openLogin, updateState, loginReturnPath: "/create/edit-rule", sessionUser: signedIn, }), ); await act(async () => { await result.current.finalize(); }); expect(router.push).toHaveBeenCalledWith("/create/completed"); expect(publishRule).not.toHaveBeenCalled(); expect(updatePublishedRule).toHaveBeenCalledWith( "existing-id", expect.objectContaining({ title: "Published title", summary: "Published summary", document: {}, }), ); expect(writeLastPublishedRule).toHaveBeenCalledWith({ id: "existing-id", title: "Published title", summary: "Published summary", document: {}, }); expect(updateState).toHaveBeenCalledWith({ editingPublishedRuleId: undefined, }); }); it("does not PATCH when a guest publishes an already-published rule", async () => { const { result } = renderHook(() => useCreateFlowFinalize({ state: { ...emptyState, editingPublishedRuleId: "guest-rule-1", }, router, openLogin, updateState, loginReturnPath: "/create/final-review?syncDraft=1", sessionUser: null, onGuestPublished, }), ); await act(async () => { await result.current.finalize(); }); expect(updatePublishedRule).not.toHaveBeenCalled(); expect(publishRule).not.toHaveBeenCalled(); expect(openLogin).not.toHaveBeenCalled(); expect(onGuestPublished).toHaveBeenCalledWith({ skippedInvites: false }); expect(writeLastPublishedRule).toHaveBeenCalledWith({ id: "guest-rule-1", title: "Published title", summary: "Published summary", document: {}, }); expect(sessionStorage.getItem(CREATE_FLOW_PENDING_KEEP_RULE_LOGIN_KEY)).toBe("1"); expect(router.push).toHaveBeenCalledWith("/create/completed"); }); it("opens keepRule login when a guest publish is unauthorized", async () => { vi.mocked(publishRule).mockResolvedValue({ ok: false, error: "Unauthorized", status: 401, }); const { result } = renderHook(() => useCreateFlowFinalize({ state: emptyState, router, openLogin, updateState, loginReturnPath: "/create/final-review?syncDraft=1", sessionUser: null, onGuestPublished, }), ); await act(async () => { await result.current.finalize(); }); expect(openLogin).toHaveBeenCalledWith({ variant: "keepRule", nextPath: "/create/completed", backdropVariant: "blurredYellow", }); expect(router.push).not.toHaveBeenCalled(); }); });