) => {
+ infiniteScrollProps.push(props);
+ return {props.children as React.ReactNode}
;
+ },
+}));
+
+vi.mock('@/hooks/useVideoProvider', () => ({
+ useVideoProvider: () => videoProviderResult,
+}));
+
+vi.mock('@/hooks/useFunnelcakeProfile', () => ({
+ useFunnelcakeProfile: () => ({ data: { video_count: 3, name: 'tester' }, isLoading: false }),
+}));
+
+vi.mock('@/hooks/useAuthor', () => ({
+ useAuthor: () => ({ data: { metadata: { name: 'tester' } }, isLoading: false }),
+}));
+
+vi.mock('@/hooks/useProfileJoinedDate', () => ({
+ useProfileJoinedDate: () => ({ data: null, isLoading: false }),
+}));
+
+vi.mock('@/hooks/useClassicVineArchiveStats', () => ({
+ useClassicVineArchiveStats: () => ({ data: undefined, isLoading: false }),
+}));
+
+vi.mock('@/hooks/useNip05Validation', () => ({
+ useNip05Validation: () => ({ isValid: false }),
+}));
+
+vi.mock('@/hooks/useRssFeedAvailable', () => ({
+ useRssFeedAvailable: () => ({ data: false }),
+}));
+
+vi.mock('@/hooks/useResolveSubdomainPubkey', () => ({
+ useResolveSubdomainPubkey: () => ({ pubkey: undefined, isLoading: false }),
+}));
+
+vi.mock('@/hooks/useNip05Pubkey', () => ({
+ useNip05Pubkey: () => ({ data: undefined, isLoading: false }),
+}));
+
+vi.mock('@/hooks/useSubdomainUser', () => ({
+ getSubdomainUser: () => null,
+}));
+
+// Everything below is chrome around the grid. Stubbing it keeps this test on
+// the scroll wiring instead of the whole profile page's data fetching.
+vi.mock('@/components/ProfileHeader', () => ({
+ ProfileHeader: () => ,
+}));
+
+vi.mock('@/components/PinnedVideosSection', () => ({
+ PinnedVideosSection: () => null,
+}));
+
+vi.mock('@/components/ProfileListsSection', () => ({
+ ProfileListsSection: () => null,
+}));
+
+vi.mock('@/components/VideoGrid', () => ({
+ VideoGrid: ({ videos }: { videos: unknown[] }) => (
+
+ ),
+}));
+
+vi.mock('@/components/VideoFeed', () => ({
+ VideoFeed: () => null,
+}));
+
+describe('ProfilePage infinite scroll', () => {
+ beforeEach(() => {
+ infiniteScrollProps.length = 0;
+ vi.clearAllMocks();
+ });
+
+ it('passes the fetched row count as dataLength, not the deduplicated length', async () => {
+ const { ProfilePage } = await import('./ProfilePage');
+
+ render(
+
+
+
+ );
+
+ const props = infiniteScrollProps.at(-1);
+ expect(props).toBeDefined();
+ // Rendered grid shows 2 unique videos; 3 rows were fetched. Using the
+ // rendered length here is what stalls the feed, because the second page
+ // leaves it unchanged and the scroll trigger never re-arms.
+ expect(props?.dataLength).toBe(3);
+ expect(props?.hasMore).toBe(true);
+ // ...while the grid itself still renders only the deduplicated videos.
+ expect(screen.getByTestId('video-grid')).toHaveAttribute('data-count', '2');
+ // Rendering the whole page pulls in a large module graph; the default 5s
+ // budget is tight for it when the suite runs in parallel.
+ }, 20_000);
+});
diff --git a/src/pages/ProfilePage.tsx b/src/pages/ProfilePage.tsx
index b70c2298..80c24b84 100644
--- a/src/pages/ProfilePage.tsx
+++ b/src/pages/ProfilePage.tsx
@@ -512,6 +512,9 @@ export function ProfilePage({ pubkeyOverride }: { pubkeyOverride?: string } = {}
) : viewMode === 'grid' ? (