From f8fcc0be57fa7f471ffd22d4b9559cb6d0ff20bf Mon Sep 17 00:00:00 2001
From: Adam Mathes <adam@adammathes.com>
Date: Fri, 13 Feb 2026 11:45:02 -0800
Subject: Implement infinite scroll for feed items view (NK-5ocxgm)

---
 frontend/src/components/FeedItems.test.tsx | 55 ++++++++++++++++++++++++++++--
 1 file changed, 53 insertions(+), 2 deletions(-)

(limited to 'frontend/src/components/FeedItems.test.tsx')

diff --git a/frontend/src/components/FeedItems.test.tsx b/frontend/src/components/FeedItems.test.tsx
index 00118fa..ea68a7c 100644
--- a/frontend/src/components/FeedItems.test.tsx
+++ b/frontend/src/components/FeedItems.test.tsx
@@ -53,8 +53,6 @@ describe('FeedItems Component', () => {
 
         await waitFor(() => {
             expect(screen.getByText('Item One')).toBeInTheDocument();
-            // Title should now be "Feed Items" based on logic
-            expect(screen.getByText('Feed Items')).toBeInTheDocument();
         });
 
         const params = new URLSearchParams();
@@ -171,4 +169,57 @@ describe('FeedItems Component', () => {
             }));
         });
     });
+
+    it('loads more items when sentinel becomes visible', async () => {
+        const initialItems = [{ _id: 101, title: 'Item 1', url: 'u1', read: true, starred: false }];
+        const moreItems = [{ _id: 100, title: 'Item 0', url: 'u0', read: true, starred: false }];
+
+        (global.fetch as any)
+            .mockResolvedValueOnce({ ok: true, json: async () => initialItems })
+            .mockResolvedValueOnce({ ok: true, json: async () => moreItems });
+
+        let observerCallback: IntersectionObserverCallback = () => { };
+        class MockIntersectionObserver {
+            constructor(callback: IntersectionObserverCallback) {
+                observerCallback = callback;
+            }
+            observe = vi.fn();
+            unobserve = vi.fn();
+            disconnect = vi.fn();
+        }
+        window.IntersectionObserver = MockIntersectionObserver as any;
+
+        render(
+            <MemoryRouter>
+                <FeedItems />
+            </MemoryRouter>
+        );
+
+        await waitFor(() => {
+            expect(screen.getByText('Item 1')).toBeInTheDocument();
+        });
+
+        // Simulate sentinel becoming visible
+        const entry = {
+            isIntersecting: true,
+            target: { id: 'load-more-sentinel' } as unknown as Element,
+            boundingClientRect: {} as DOMRectReadOnly,
+            intersectionRatio: 1,
+            time: 0,
+            rootBounds: null,
+            intersectionRect: {} as DOMRectReadOnly,
+        } as IntersectionObserverEntry;
+
+        act(() => {
+            observerCallback([entry], {} as IntersectionObserver);
+        });
+
+        await waitFor(() => {
+            expect(screen.getByText('Item 0')).toBeInTheDocument();
+            const params = new URLSearchParams();
+            params.append('max_id', '101');
+            params.append('read_filter', 'unread');
+            expect(global.fetch).toHaveBeenCalledWith(`/api/stream?${params.toString()}`);
+        });
+    });
 });
-- 
cgit v1.2.3

