import { label1BoldOptions, useProcessedChecklist, } from "./use-processed-check-list"; import { MARKS } from "@contentful/rich-text-types"; import { renderHook } from "@testing-library/react"; // Mock renderContentfulRichText to return predictable output jest.mock("./use-contentful-rich-text", () => ({ renderContentfulRichText: jest.fn( (doc: any, _target?: boolean, className?: string) => { // Return a simple string representation based on the doc if (!doc) return null; // Extract text from the doc content if available const text = doc?.content?.[0]?.content?.[0]?.value; return text ? `rendered:${text}:${className ?? ""}` : null; } ), })); jest.mock("@shared/utils/contentful/to-document", () => ({ toDocument: jest.fn((v: any) => { if (!v) return null; if (typeof v === "string") { return { nodeType: "document", data: {}, content: [ { nodeType: "paragraph", data: {}, content: [{ nodeType: "text", value: v, marks: [], data: {} }], }, ], }; } return v; }), })); const makeChecklistData = (items: any[]) => ({ list: { items, }, }); describe("useProcessedChecklist", () => { describe("Full items mode (titlesOnly=false)", () => { it("returns empty array when checklistData is undefined", () => { const { result } = renderHook(() => useProcessedChecklist(undefined)); expect(result.current).toEqual([]); }); it("returns empty array when checklistData has no list", () => { const { result } = renderHook(() => useProcessedChecklist({})); expect(result.current).toEqual([]); }); it("returns empty array when list.items is empty", () => { const data = makeChecklistData([]); const { result } = renderHook(() => useProcessedChecklist(data)); expect(result.current).toEqual([]); }); it("returns ProcessedChecklistItem array with title, iconUrl, anchorId", () => { const data = makeChecklistData([ { checkListTitle: "Item 1", icon: { url: "https://cdn.com/icon1.png" }, anchorId: "anchor-1", }, { checkListTitle: "Item 2", icon: { url: "https://cdn.com/icon2.png" }, anchorId: "anchor-2", }, ]); const { result } = renderHook(() => useProcessedChecklist(data)); expect(result.current).toHaveLength(2); expect(result.current[0]).toEqual({ title: "rendered:Item 1:", iconUrl: "https://cdn.com/icon1.png", anchorId: "anchor-1", }); expect(result.current[1]).toEqual({ title: "rendered:Item 2:", iconUrl: "https://cdn.com/icon2.png", anchorId: "anchor-2", }); }); it("sets iconUrl to undefined when icon is missing", () => { const data = makeChecklistData([ { checkListTitle: "No icon", icon: undefined }, ]); const { result } = renderHook(() => useProcessedChecklist(data)); expect(result.current[0].iconUrl).toBeUndefined(); }); it("sets anchorId to undefined when anchorId is missing", () => { const data = makeChecklistData([{ checkListTitle: "No anchor" }]); const { result } = renderHook(() => useProcessedChecklist(data)); expect(result.current[0].anchorId).toBeUndefined(); }); it("uses empty string for title when checkListTitle is null", () => { const data = makeChecklistData([{ checkListTitle: null }]); const { result } = renderHook(() => useProcessedChecklist(data)); // toDocument("") returns null, renderContentfulRichText returns null, falls back to "" expect(result.current[0].title).toBe(""); }); it("handles item with undefined checkListTitle", () => { const data = makeChecklistData([{}]); const { result } = renderHook(() => useProcessedChecklist(data)); expect(result.current[0].title).toBe(""); }); it("handles null item in items array", () => { const data = makeChecklistData([null]); const { result } = renderHook(() => useProcessedChecklist(data)); expect(result.current[0].title).toBe(""); expect(result.current[0].iconUrl).toBeUndefined(); expect(result.current[0].anchorId).toBeUndefined(); }); it("handles undefined item in items array", () => { const data = makeChecklistData([undefined]); const { result } = renderHook(() => useProcessedChecklist(data)); expect(result.current[0].title).toBe(""); }); it("passes richTextClassName to renderContentfulRichText in full mode", () => { const data = makeChecklistData([{ checkListTitle: "Styled" }]); const { result } = renderHook(() => useProcessedChecklist(data, false, undefined, "full-class") ); expect(result.current[0].title).toBe("rendered:Styled:full-class"); }); it("passes richTextOptions in full mode", () => { const data = makeChecklistData([{ checkListTitle: "Opts" }]); const customOpts = { renderMark: { [MARKS.BOLD]: (t: any) => t } }; const { result } = renderHook(() => useProcessedChecklist(data, false, customOpts, "opts-class") ); expect(result.current[0].title).toBe("rendered:Opts:opts-class"); }); it("handles item with icon but no url", () => { const data = makeChecklistData([{ checkListTitle: "No url", icon: {} }]); const { result } = renderHook(() => useProcessedChecklist(data)); expect(result.current[0].iconUrl).toBeUndefined(); }); }); describe("Titles only mode (titlesOnly=true)", () => { it("returns string array of rendered titles", () => { const data = makeChecklistData([ { checkListTitle: "Title A" }, { checkListTitle: "Title B" }, ]); const { result } = renderHook(() => useProcessedChecklist(data, true)); expect(result.current).toHaveLength(2); expect(result.current[0]).toBe("rendered:Title A:"); expect(result.current[1]).toBe("rendered:Title B:"); }); it("returns empty string for null checkListTitle", () => { const data = makeChecklistData([{ checkListTitle: null }]); const { result } = renderHook(() => useProcessedChecklist(data, true)); expect(result.current[0]).toBe(""); }); it("returns empty array when no items", () => { const { result } = renderHook(() => useProcessedChecklist(undefined, true) ); expect(result.current).toEqual([]); }); it("handles null item in items array in titlesOnly mode", () => { const data = makeChecklistData([null]); const { result } = renderHook(() => useProcessedChecklist(data, true)); expect(result.current[0]).toBe(""); }); it("handles undefined item in items array in titlesOnly mode", () => { const data = makeChecklistData([undefined]); const { result } = renderHook(() => useProcessedChecklist(data, true)); expect(result.current[0]).toBe(""); }); it("passes richTextOptions in titlesOnly mode", () => { const data = makeChecklistData([{ checkListTitle: "Opt" }]); const customOpts = { renderMark: {} }; const { result } = renderHook(() => useProcessedChecklist(data, true, customOpts, "title-class") ); expect(result.current[0]).toBe("rendered:Opt:title-class"); }); }); describe("richTextOptions and richTextClassName", () => { it("passes richTextClassName to renderContentfulRichText", () => { const data = makeChecklistData([{ checkListTitle: "Styled" }]); const { result } = renderHook(() => useProcessedChecklist(data, true, undefined, "custom-class") ); expect(result.current[0]).toBe("rendered:Styled:custom-class"); }); it("passes richTextOptions through", () => { const data = makeChecklistData([{ checkListTitle: "Options" }]); const customOpts = { renderMark: {} }; const { result } = renderHook(() => useProcessedChecklist(data, false, customOpts) ); // Should still produce results (richTextOptions is passed but our mock ignores it) expect(result.current).toHaveLength(1); }); }); describe("Memoization", () => { it("returns same reference when inputs don't change", () => { const data = makeChecklistData([{ checkListTitle: "Memo" }]); const { result, rerender } = renderHook( ({ d }) => useProcessedChecklist(d), { initialProps: { d: data } } ); const first = result.current; rerender({ d: data }); expect(result.current).toBe(first); }); it("recomputes when checklistData changes", () => { const data1 = makeChecklistData([{ checkListTitle: "V1" }]); const data2 = makeChecklistData([{ checkListTitle: "V2" }]); const { result, rerender } = renderHook( ({ d }) => useProcessedChecklist(d), { initialProps: { d: data1 } } ); const first = result.current; rerender({ d: data2 }); expect(result.current).not.toBe(first); }); it("recomputes when titlesOnly changes", () => { const data = makeChecklistData([{ checkListTitle: "Toggle" }]); const { result, rerender } = renderHook( ({ t }) => useProcessedChecklist(data, t), { initialProps: { t: false as boolean } } ); const first = result.current; rerender({ t: true }); expect(result.current).not.toBe(first); }); }); }); describe("label1BoldOptions", () => { it("exports BOLD mark renderer that wraps in span with label1 class", () => { const renderer = label1BoldOptions.renderMark[MARKS.BOLD]; expect(renderer).toBeDefined(); }); it("renders bold text in a span with label1 className", () => { const renderer = label1BoldOptions.renderMark[MARKS.BOLD]; const result = renderer("Bold text"); // createElement returns a React element with type 'span' and className 'label1' expect(result).toBeDefined(); expect((result as any).type).toBe("span"); expect((result as any).props.className).toBe("label1"); expect((result as any).props.children).toBe("Bold text"); }); });