import { describe, it, expect } from "vitest"; import { buildPublishPayload, parseDocumentSectionsForDisplay, parseSectionsFromCreateFlowState, } from "../../lib/create/buildPublishPayload"; import { mergeCoreValueDetailWithPresets } from "../../lib/create/finalReviewChipPresets"; import type { CreateFlowState } from "../../app/(app)/create/types"; describe("buildPublishPayload", () => { it("returns error when title missing", () => { expect(buildPublishPayload({})).toEqual({ ok: false, error: "missingCommunityName", }); }); it("returns error when title is whitespace only", () => { expect(buildPublishPayload({ title: " \n\t " })).toEqual({ ok: false, error: "missingCommunityName", }); }); it("returns title and fallback Overview section when no sections", () => { const r = buildPublishPayload({ title: "Oak Park Commons" }); expect(r.ok).toBe(true); if (!r.ok) return; expect(r.title).toBe("Oak Park Commons"); expect(r.summary).toBeUndefined(); expect(r.document).toEqual({ sections: [ { categoryName: "Overview", entries: [ { title: "Community", body: "This CommunityRule was created in the create flow. Add more detail in a future edit.", }, ], }, ], coreValues: [], }); }); it("includes trimmed summary in payload and uses it as fallback section body", () => { const r = buildPublishPayload({ title: " My Group ", summary: " We organize locally. ", }); expect(r.ok).toBe(true); if (!r.ok) return; expect(r.title).toBe("My Group"); expect(r.summary).toBe("We organize locally."); expect(r.document).toEqual({ sections: [ { categoryName: "Overview", entries: [{ title: "Community", body: "We organize locally." }], }, ], coreValues: [], }); }); it("prefers communityContext over summary for the published summary field", () => { const r = buildPublishPayload({ title: "T", summary: "One-liner or leftover", communityContext: " Full community context. ", }); expect(r.ok).toBe(true); if (!r.ok) return; expect(r.summary).toBe("Full community context."); }); it("uses valid state.sections when present", () => { const sections: CreateFlowState["sections"] = [ { categoryName: "Values", entries: [{ title: "A", body: "B" }], }, ]; const r = buildPublishPayload({ title: "T", sections }); expect(r.ok).toBe(true); if (!r.ok) return; expect(r.document).toEqual({ sections, coreValues: [] }); }); it("filters invalid section entries from state.sections", () => { const r = buildPublishPayload({ title: "T", sections: [ { categoryName: "Values", entries: [{ title: "A", body: "B" }] }, { bad: true } as unknown as Record, ], }); expect(r.ok).toBe(true); if (!r.ok) return; expect(r.document).toEqual({ sections: [{ categoryName: "Values", entries: [{ title: "A", body: "B" }] }], coreValues: [], }); }); it("includes coreValues from selected chips and detail text", () => { const r = buildPublishPayload({ title: "T", selectedCoreValueIds: ["1", "2"], coreValuesChipsSnapshot: [ { id: "1", label: "Alpha", state: "selected" }, { id: "2", label: "Beta", state: "selected" }, ], coreValueDetailsByChipId: { "1": { meaning: "m1", signals: "s1" }, }, }); expect(r.ok).toBe(true); if (!r.ok) return; const preset2 = mergeCoreValueDetailWithPresets("2", "Beta", undefined); expect(r.document.coreValues).toEqual([ { chipId: "1", label: "Alpha", meaning: "m1", signals: "s1" }, { chipId: "2", label: "Beta", meaning: preset2.meaning, signals: preset2.signals, }, ]); }); }); describe("buildPublishPayload — methodSelections", () => { it("omits document.methodSelections when no method group is selected", () => { const r = buildPublishPayload({ title: "T" }); expect(r.ok).toBe(true); if (!r.ok) return; expect(r.document.methodSelections).toBeUndefined(); }); it("derives methodSelections from template sections when selected ids are empty", () => { const r = buildPublishPayload({ title: "T", sections: [ { categoryName: "Communication", entries: [{ title: "Slack", body: "" }], }, ], }); expect(r.ok).toBe(true); if (!r.ok) return; const ms = r.document.methodSelections as | { communication?: Array<{ id: string; sections: { corePrinciple: string }; }>; } | undefined; expect(ms?.communication?.length).toBe(1); expect(ms?.communication?.[0]?.id).toBe("slack"); const first = ms?.communication?.[0]; expect(first?.sections.corePrinciple.length).toBeGreaterThan(10); const entries = (r.document.sections as Array<{ entries: Array<{ blocks?: unknown[] }> }>)[0] ?.entries; expect(entries?.[0]?.blocks?.length).toBeGreaterThanOrEqual(1); }); it("uses customMethodCardMetaById label when preset id is unknown", () => { const customId = "00000000-0000-4000-8000-000000000002"; const r = buildPublishPayload({ title: "T", selectedCommunicationMethodIds: [customId], customMethodCardMetaById: { [customId]: { label: "Custom Comm", supportText: "More" }, }, }); expect(r.ok).toBe(true); if (!r.ok) return; const ms = r.document.methodSelections as | { communication?: Array<{ id: string; label: string }> } | undefined; expect(ms?.communication?.length).toBe(1); expect(ms?.communication?.[0]?.id).toBe(customId); expect(ms?.communication?.[0]?.label).toBe("Custom Comm"); }); it("embeds wizard field blocks in published Communication sections for custom UUID ids", () => { const customId = "00000000-0000-4000-8000-000000000099"; const r = buildPublishPayload({ title: "T", selectedCommunicationMethodIds: [customId], sections: [ { categoryName: "Communication", entries: [{ title: "Template row", body: "placeholder" }], }, ], customMethodCardMetaById: { [customId]: { label: "Wizard title", supportText: "" }, }, customMethodCardFieldBlocksById: { [customId]: [ { kind: "text", id: "b1", blockTitle: "Field A", placeholderText: "User-authored body", }, ], }, }); expect(r.ok).toBe(true); if (!r.ok) return; const secs = r.document.sections as Array<{ categoryName: string; entries: Array<{ blocks?: Array<{ label: string; body: string }> }>; }>; const comm = secs.find((s) => s.categoryName === "Communication"); expect(comm?.entries[0]?.blocks).toEqual([ { label: "Field A", body: "User-authored body" }, ]); }); it("emits preset-only sections when a method is selected without an override", () => { const r = buildPublishPayload({ title: "T", selectedCommunicationMethodIds: ["signal"], }); expect(r.ok).toBe(true); if (!r.ok) return; const ms = r.document.methodSelections as | Record>> | undefined; expect(ms).toBeDefined(); expect(ms?.communication?.length).toBe(1); const entry = ms?.communication?.[0] as { id: string; label: string; sections: { corePrinciple: string }; }; expect(entry.id).toBe("signal"); expect(entry.label).toBe("Signal"); // Preset corePrinciple is non-empty for `signal` in the shipped messages // file — proves we read presets when no override is present. expect(entry.sections.corePrinciple.length).toBeGreaterThan(0); }); it("merges override on top of preset for the selected method", () => { const r = buildPublishPayload({ title: "T", selectedCommunicationMethodIds: ["signal"], communicationMethodDetailsById: { signal: { corePrinciple: "OVERRIDE PRINCIPLE", logisticsAdmin: "OVERRIDE LOGISTICS", codeOfConduct: "OVERRIDE COC", }, }, }); expect(r.ok).toBe(true); if (!r.ok) return; const ms = r.document.methodSelections as | Record>> | undefined; const entry = ms?.communication?.[0] as { sections: { corePrinciple: string; logisticsAdmin: string; codeOfConduct: string; }; }; expect(entry.sections.corePrinciple).toBe("OVERRIDE PRINCIPLE"); expect(entry.sections.logisticsAdmin).toBe("OVERRIDE LOGISTICS"); expect(entry.sections.codeOfConduct).toBe("OVERRIDE COC"); }); it("emits a methodSelections entry per selected group", () => { const r = buildPublishPayload({ title: "T", selectedCommunicationMethodIds: ["signal"], selectedMembershipMethodIds: ["open-access"], selectedDecisionApproachIds: ["lazy-consensus"], selectedConflictManagementIds: ["peer-mediation"], }); expect(r.ok).toBe(true); if (!r.ok) return; const ms = r.document.methodSelections as | Record> | undefined; expect(Object.keys(ms ?? {}).sort()).toEqual([ "communication", "conflictManagement", "decisionApproaches", "membership", ]); }); }); describe("parseDocumentSectionsForDisplay", () => { it("returns empty for non-object", () => { expect(parseDocumentSectionsForDisplay(null)).toEqual([]); }); it("parses valid sections array", () => { const doc = { sections: [ { categoryName: "X", entries: [{ title: "t", body: "b" }] }, ], }; expect(parseDocumentSectionsForDisplay(doc)).toEqual(doc.sections); }); it("accepts entries with labeled blocks and omits body in JSON (normalized to \"\")", () => { const doc = { sections: [ { categoryName: "Membership", entries: [ { title: "Open membership", blocks: [ { label: "Eligibility", body: "Anyone may join." }, { label: "Process", body: "Sign the sheet." }, ], }, ], }, ], }; expect(parseDocumentSectionsForDisplay(doc)).toEqual([ { categoryName: "Membership", entries: [ { title: "Open membership", body: "", blocks: [ { label: "Eligibility", body: "Anyone may join." }, { label: "Process", body: "Sign the sheet." }, ], }, ], }, ]); }); it("accepts entries with labeled blocks and empty body", () => { const doc = { sections: [ { categoryName: "Membership", entries: [ { title: "Open membership", body: "", blocks: [ { label: "Eligibility", body: "Anyone may join." }, { label: "Process", body: "Sign the sheet." }, ], }, ], }, ], }; expect(parseDocumentSectionsForDisplay(doc)).toEqual(doc.sections); }); it("still parses entries with empty body and no blocks", () => { const doc = { sections: [ { categoryName: "Values", entries: [{ title: "Consensus", body: "" }], }, ], }; expect(parseDocumentSectionsForDisplay(doc)).toEqual(doc.sections); }); }); describe("parseSectionsFromCreateFlowState", () => { it("returns empty when sections missing", () => { expect(parseSectionsFromCreateFlowState({})).toEqual([]); }); });