From 0883ea666b9c771561536bad0d474c3441323c11 Mon Sep 17 00:00:00 2001 From: pitoi Date: Fri, 12 Jun 2026 16:31:45 +0000 Subject: [PATCH] Generated with Hive: Fix ScrollArea root height to restore scrolling and pagination --- src/components/layout/my-content-panel.tsx | 2 +- src/components/ui/scroll-area.tsx | 2 +- src/lib/__tests__/my-content-page.test.tsx | 93 ++++++++++++++++++++++ src/lib/__tests__/setup.ts | 18 +++++ 4 files changed, 113 insertions(+), 2 deletions(-) diff --git a/src/components/layout/my-content-panel.tsx b/src/components/layout/my-content-panel.tsx index 819b5f3c..34577758 100644 --- a/src/components/layout/my-content-panel.tsx +++ b/src/components/layout/my-content-panel.tsx @@ -457,7 +457,7 @@ export function MyContentPanel({ onClose }: { onClose: () => void }) { ) })} {hasMore && ( -
+
{loadingMore && }
)} diff --git a/src/components/ui/scroll-area.tsx b/src/components/ui/scroll-area.tsx index 84c1e9fb..8ac8b356 100644 --- a/src/components/ui/scroll-area.tsx +++ b/src/components/ui/scroll-area.tsx @@ -13,7 +13,7 @@ function ScrollArea({ return ( ({ deleteNode: (...args: unknown[]) => mockDeleteNode(...args), })) +// --- mock creator-insights (avoids api.get side-channel in tests) --- +vi.mock("@/lib/creator-insights", () => ({ + fetchCreatorInsights: () => Promise.resolve({ period: "week", total_sats_earned: 0, total_unlocks: 0, nodes: [] }), + getGrowthBadge: () => "flat", +})) + // --- mock stores --- interface MyContentUserStore { pubKey: string @@ -571,6 +577,93 @@ describe("MyContentPanel — myContentRefreshKey re-fetch", () => { }) }) +describe("MyContentPanel — ScrollArea h-full class", () => { + beforeEach(() => { + vi.clearAllMocks() + myContentUserOverrides = {} + }) + + it("ScrollArea root element always has h-full class", async () => { + mockApiGet.mockResolvedValue(TWO_NODES) + const { container } = render( {}} />) + await waitFor(() => expect(screen.getByText("Bitcoin is freedom")).toBeInTheDocument()) + const scrollAreaRoots = container.querySelectorAll("[data-slot='scroll-area']") + expect(scrollAreaRoots.length).toBeGreaterThan(0) + scrollAreaRoots.forEach((el) => { + expect(el.classList.contains("h-full")).toBe(true) + }) + }) +}) + +describe("MyContentPanel — infinite scroll pagination", () => { + beforeEach(() => { + vi.clearAllMocks() + myContentUserOverrides = { pubKey: "03abc123testkey", routeHint: "", isAdmin: false } + }) + + const makeNodes = (count: number, offset = 0) => ({ + nodes: Array.from({ length: count }, (_, i) => ({ + node_type: "Tweet", + ref_id: `ref-${offset + i}`, + properties: { name: `Node ${offset + i}`, status: "done" }, + })), + totalCount: count, + totalProcessing: 0, + }) + + it("sentinel div is present when hasMore is true", async () => { + // First page returns PAGE_SIZE (50) nodes → hasMore = true + mockApiGet.mockResolvedValueOnce(makeNodes(50)) + const { container } = render( {}} />) + await waitFor(() => expect(screen.getByText("Node 0")).toBeInTheDocument()) + // sentinel is the div inside the ScrollArea that the IntersectionObserver watches + // it only renders when hasMore === true + const sentinel = container.querySelector("[data-testid='sentinel']") ?? + // fallback: find the loader or any trailing div with ref + Array.from(container.querySelectorAll("[data-slot='scroll-area-viewport'] > div > div")).at(-1) + // The last child of the list should be the hasMore sentinel + expect(sentinel).toBeTruthy() + }) + + it("loadMore fetches next page with correct skip param", async () => { + // First page: 50 nodes → hasMore true + mockApiGet.mockResolvedValueOnce(makeNodes(50)) + // Second page: 10 nodes → hasMore false + mockApiGet.mockResolvedValueOnce(makeNodes(10, 50)) + + // Mock IntersectionObserver to immediately trigger intersection. + // Must be a class (constructable) because @base-ui uses `new IntersectionObserver()` internally. + const observerCallbacks: ((entries: IntersectionObserverEntry[]) => void)[] = [] + class MockIntersectionObserver { + constructor(cb: (entries: IntersectionObserverEntry[]) => void) { + observerCallbacks.push(cb) + } + observe = vi.fn() + disconnect = vi.fn() + } + vi.stubGlobal("IntersectionObserver", MockIntersectionObserver) + + render( {}} />) + await waitFor(() => expect(screen.getByText("Node 0")).toBeInTheDocument()) + + // Simulate intersection (sentinel enters viewport) + act(() => { + observerCallbacks.forEach((cb) => + cb([{ isIntersecting: true } as IntersectionObserverEntry]) + ) + }) + + await waitFor(() => { + expect(mockApiGet).toHaveBeenCalledWith("/v2/content?sort_by=date&limit=50&skip=50") + }) + + // Second page nodes now rendered + await waitFor(() => expect(screen.getByText("Node 50")).toBeInTheDocument()) + + vi.unstubAllGlobals() + }) +}) + describe("MyContentPanel — Socket.IO integration", () => { beforeEach(() => { vi.clearAllMocks() diff --git a/src/lib/__tests__/setup.ts b/src/lib/__tests__/setup.ts index df6631ee..af427ff3 100644 --- a/src/lib/__tests__/setup.ts +++ b/src/lib/__tests__/setup.ts @@ -1 +1,19 @@ import "@testing-library/jest-dom" + +// Global IntersectionObserver stub — jsdom doesn't provide one. +// @base-ui ScrollArea uses new IntersectionObserver() internally, so we need +// a class that can be instantiated, not just a plain function mock. +class IntersectionObserverStub { + private cb: (entries: IntersectionObserverEntry[]) => void + constructor(cb: (entries: IntersectionObserverEntry[]) => void) { + this.cb = cb + } + observe() {} + unobserve() {} + disconnect() {} +} +Object.defineProperty(globalThis, "IntersectionObserver", { + writable: true, + configurable: true, + value: IntersectionObserverStub, +})