diff --git a/src/hooks/communities.test.ts b/src/hooks/communities.test.ts index ccb83af8..9f4c4cc6 100644 --- a/src/hooks/communities.test.ts +++ b/src/hooks/communities.test.ts @@ -532,6 +532,136 @@ describe("communities", () => { expect(rendered.result.current.hourActiveUserCount).toBe(1); }); + test("useCommunityStats unwraps fetchCid content responses", async () => { + let fetchSpy: { mockRestore: () => void } | undefined; + try { + fetchSpy = vi.spyOn(PKC.prototype as any, "fetchCid").mockResolvedValue({ + content: { + hourActiveUserCount: 3, + weekActiveUserCount: 33, + allPostCount: 333, + }, + }); + const rendered = renderHook(() => + useCommunityStats({ community: { name: "wrapped stats" } }), + ); + const waitFor = testUtils.createWaitFor(rendered); + await waitFor(() => rendered.result.current.state === "succeeded"); + expect(rendered.result.current.hourActiveUserCount).toBe(3); + expect(rendered.result.current.weekActiveUserCount).toBe(33); + expect(rendered.result.current.allPostCount).toBe(333); + } finally { + fetchSpy?.mockRestore(); + } + }); + + test("useCommunityStats parses string fetchCid content responses", async () => { + let fetchSpy: { mockRestore: () => void } | undefined; + try { + fetchSpy = vi.spyOn(PKC.prototype as any, "fetchCid").mockResolvedValue({ + content: JSON.stringify({ + hourActiveUserCount: 4, + weekActiveUserCount: 44, + allPostCount: 444, + }), + }); + const rendered = renderHook(() => + useCommunityStats({ community: { name: "string wrapped stats" } }), + ); + const waitFor = testUtils.createWaitFor(rendered); + await waitFor(() => rendered.result.current.state === "succeeded"); + expect(rendered.result.current.hourActiveUserCount).toBe(4); + expect(rendered.result.current.weekActiveUserCount).toBe(44); + expect(rendered.result.current.allPostCount).toBe(444); + } finally { + fetchSpy?.mockRestore(); + } + }); + + test("useCommunityStats parses string fetchCid responses", async () => { + let fetchSpy: { mockRestore: () => void } | undefined; + try { + fetchSpy = vi.spyOn(PKC.prototype as any, "fetchCid").mockResolvedValue( + JSON.stringify({ + hourActiveUserCount: 5, + weekActiveUserCount: 55, + allPostCount: 555, + }), + ); + const rendered = renderHook(() => + useCommunityStats({ community: { name: "string stats" } }), + ); + const waitFor = testUtils.createWaitFor(rendered); + await waitFor(() => rendered.result.current.state === "succeeded"); + expect(rendered.result.current.hourActiveUserCount).toBe(5); + expect(rendered.result.current.weekActiveUserCount).toBe(55); + expect(rendered.result.current.allPostCount).toBe(555); + } finally { + fetchSpy?.mockRestore(); + } + }); + + test("useCommunityStats accepts object fetchCid responses", async () => { + let fetchSpy: { mockRestore: () => void } | undefined; + try { + fetchSpy = vi.spyOn(PKC.prototype as any, "fetchCid").mockResolvedValue({ + hourActiveUserCount: 6, + weekActiveUserCount: 66, + allPostCount: 666, + }); + const rendered = renderHook(() => + useCommunityStats({ community: { name: "object stats" } }), + ); + const waitFor = testUtils.createWaitFor(rendered); + await waitFor(() => rendered.result.current.state === "succeeded"); + expect(rendered.result.current.hourActiveUserCount).toBe(6); + expect(rendered.result.current.weekActiveUserCount).toBe(66); + expect(rendered.result.current.allPostCount).toBe(666); + } finally { + fetchSpy?.mockRestore(); + } + }); + + test("useCommunityStats keeps direct stats responses with content fields", async () => { + let fetchSpy: { mockRestore: () => void } | undefined; + try { + fetchSpy = vi.spyOn(PKC.prototype as any, "fetchCid").mockResolvedValue({ + content: "not json stats content", + hourActiveUserCount: 5, + weekActiveUserCount: 55, + allPostCount: 555, + }); + const rendered = renderHook(() => + useCommunityStats({ community: { name: "stats with content" } }), + ); + const waitFor = testUtils.createWaitFor(rendered); + await waitFor(() => rendered.result.current.state === "succeeded"); + expect(rendered.result.current.hourActiveUserCount).toBe(5); + expect(rendered.result.current.weekActiveUserCount).toBe(55); + expect(rendered.result.current.allPostCount).toBe(555); + expect(rendered.result.current.content).toBe("not json stats content"); + } finally { + fetchSpy?.mockRestore(); + } + }); + + test("useCommunityStats keeps unrecognized content responses", async () => { + let fetchSpy: { mockRestore: () => void } | undefined; + try { + fetchSpy = vi.spyOn(PKC.prototype as any, "fetchCid").mockResolvedValue({ + content: "not json stats content", + }); + const rendered = renderHook(() => + useCommunityStats({ community: { name: "unknown content stats" } }), + ); + const waitFor = testUtils.createWaitFor(rendered); + await waitFor(() => rendered.result.current.state === "succeeded"); + expect(rendered.result.current.content).toBe("not json stats content"); + } finally { + fetchSpy?.mockRestore(); + } + }); + test("useCommunityStats fetchCid error logs (stmt 110)", async () => { const origFetch = PKC.prototype.fetchCid; (PKC.prototype as any).fetchCid = () => Promise.reject(new Error("fetchCid failed")); diff --git a/src/hooks/communities.ts b/src/hooks/communities.ts index 30fcb721..217b617e 100644 --- a/src/hooks/communities.ts +++ b/src/hooks/communities.ts @@ -26,6 +26,37 @@ import shallow from "zustand/shallow"; import { getChainProviders, getPkcCommunityAddresses } from "../lib/pkc-compat"; import { getCommunityRefKey, getUniqueSortedCommunityRefs } from "../lib/community-ref"; +const parseMaybeJson = (value: unknown) => (typeof value === "string" ? JSON.parse(value) : value); + +const tryParseMaybeJson = (value: unknown) => { + try { + return parseMaybeJson(value); + } catch { + return value; + } +}; + +const isRecord = (value: unknown): value is Record => + !!value && typeof value === "object"; + +const isCommunityStatsPayload = (value: unknown): value is CommunityStats => + isRecord(value) && + ("hourActiveUserCount" in value || "weekActiveUserCount" in value || "allPostCount" in value); + +const parseFetchedCommunityStats = (fetchedCid: unknown): CommunityStats => { + const parsedCid = parseMaybeJson(fetchedCid); + if (isCommunityStatsPayload(parsedCid)) { + return parsedCid; + } + if (isRecord(parsedCid) && "content" in parsedCid) { + const parsedContent = tryParseMaybeJson(parsedCid.content); + if (isCommunityStatsPayload(parsedContent)) { + return parsedContent; + } + } + return parsedCid as CommunityStats; +}; + /** * @param community - The community identifier, e.g. {name: 'memes.eth'} or {publicKey: '12D3KooW...'} * @param acountName - The nickname of the account, e.g. 'Account 1'. If no accountName is provided, use @@ -183,7 +214,7 @@ export function useCommunityStats(options?: UseCommunityStatsOptions): UseCommun let fetchedCid; try { fetchedCid = await account.pkc.fetchCid({ cid: communityStatsCid }); - fetchedCid = JSON.parse(fetchedCid); + fetchedCid = parseFetchedCommunityStats(fetchedCid); if (cancelled) { return; }