Files
community-rule/tests/unit/hooks/useCreateFlowFinalize.test.tsx
T
adilalloandCursor db34a1f0df Let guests keep a published rule via Save & Exit and a post-finalize sign-in prompt.
Email is optional so they can continue without saving; skip from Save & Exit leaves the flow instead of returning to completed.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-02 16:18:41 -06:00

326 lines
9.2 KiB
TypeScript

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