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 { buildPublishPayload } from "../../../lib/create/buildPublishPayload"; import { stateWithPendingCommunityAvatar } from "../../../lib/create/flushPendingCommunityAvatar"; import messages from "../../../messages/en/index"; 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/flushPendingCommunityAvatar", () => ({ stateWithPendingCommunityAvatar: vi.fn(async (state: CreateFlowState) => ({ ok: true as const, state, })), })); 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(); vi.mocked(stateWithPendingCommunityAvatar).mockReset(); vi.mocked(stateWithPendingCommunityAvatar).mockImplementation( async (state) => ({ ok: true as const, state }), ); 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("publishes a guest logo that was only staged in the browser", async () => { vi.mocked(publishRule).mockResolvedValue({ ok: true, id: "guest-rule-id", title: "Published title", }); vi.mocked(stateWithPendingCommunityAvatar).mockResolvedValue({ ok: true, state: { communityAvatarFileName: "logo.png", communityAvatarUrl: "/api/uploads/guest-logo", }, }); vi.mocked(buildPublishPayload).mockImplementationOnce((draft) => ({ ok: true as const, title: "Published title", summary: "Published summary", document: { communityAvatarUrl: draft.communityAvatarUrl, }, })); const { result } = renderHook(() => useCreateFlowFinalize({ state: { communityAvatarFileName: "logo.png" }, 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: { communityAvatarUrl: "/api/uploads/guest-logo" }, }); expect(updateState).toHaveBeenCalledWith({ communityAvatarUrl: "/api/uploads/guest-logo", }); }); it("does not publish when the staged logo fails to upload", async () => { vi.mocked(stateWithPendingCommunityAvatar).mockResolvedValue({ ok: false, reason: "upload_failed", }); const { result } = renderHook(() => useCreateFlowFinalize({ state: { communityAvatarFileName: "logo.png" }, router, openLogin, updateState, loginReturnPath: "/create/final-review", sessionUser: null, }), ); await act(async () => { await result.current.finalize(); }); expect(publishRule).not.toHaveBeenCalled(); expect(result.current.publishBannerMessage).toBe( messages.create.upload.errors.generic, ); expect(result.current.isPublishing).toBe(false); }); 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(); }); });