From 72e131f9c273d15e8d3b5c8a9320ab7fb1d533d4 Mon Sep 17 00:00:00 2001
From: Adam Mathes <adam@adammathes.com>
Date: Mon, 16 Feb 2026 16:35:38 -0800
Subject: Fix scroll-to-read functionality across all UIs (V1, V2, V3)

---
 frontend/src/components/FeedItems.test.tsx | 62 ++++++++++++++++++------------
 1 file changed, 38 insertions(+), 24 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 fc95948..ad8bf4f 100644
--- a/frontend/src/components/FeedItems.test.tsx
+++ b/frontend/src/components/FeedItems.test.tsx
@@ -2,7 +2,7 @@ import React from 'react';
 import '@testing-library/jest-dom';
 import { render, screen, waitFor, fireEvent, act } from '@testing-library/react';
 import { MemoryRouter, Route, Routes } from 'react-router-dom';
-import { describe, it, expect, vi, beforeEach } from 'vitest';
+import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
 import FeedItems from './FeedItems';
 
 describe('FeedItems Component', () => {
@@ -126,6 +126,11 @@ describe('FeedItems Component', () => {
     });
   });
 
+  afterEach(() => {
+    vi.useRealTimers();
+    vi.restoreAllMocks();
+  });
+
   it('marks items as read when scrolled past', async () => {
     const mockItems = [{ _id: 101, title: 'Item 1', url: 'u1', read: false, starred: false }];
     vi.mocked(global.fetch).mockResolvedValue({
@@ -133,44 +138,53 @@ describe('FeedItems Component', () => {
       json: async () => mockItems,
     } as Response);
 
-    const observerCallbacks: IntersectionObserverCallback[] = [];
-    class MockIntersectionObserver {
-      constructor(callback: IntersectionObserverCallback) {
-        observerCallbacks.push(callback);
+    // Mock getBoundingClientRect
+    const getBoundingClientRectMock = vi.spyOn(Element.prototype, 'getBoundingClientRect');
+    getBoundingClientRectMock.mockImplementation(function (this: Element) {
+      if (this.classList && this.classList.contains('dashboard-main')) {
+        return {
+          top: 0, bottom: 500, height: 500, left: 0, right: 1000, width: 1000, x: 0, y: 0,
+          toJSON: () => { }
+        } as DOMRect;
       }
-      observe = vi.fn();
-      unobserve = vi.fn();
-      disconnect = vi.fn();
-    }
-    // eslint-disable-next-line @typescript-eslint/no-explicit-any
-    window.IntersectionObserver = MockIntersectionObserver as any;
+      if (this.id && this.id.startsWith('item-')) {
+        // Item top is -50 (above container top 0)
+        return {
+          top: -50, bottom: 50, height: 100, left: 0, right: 1000, width: 1000, x: 0, y: 0,
+          toJSON: () => { }
+        } as DOMRect;
+      }
+      return {
+        top: 0, bottom: 0, height: 0, left: 0, right: 0, width: 0, x: 0, y: 0,
+        toJSON: () => { }
+      } as DOMRect;
+    });
 
     render(
       <MemoryRouter>
-        <FeedItems />
+        <div className="dashboard-main">
+          <FeedItems />
+        </div>
       </MemoryRouter>
     );
 
+    // Initial load fetch
     await waitFor(() => {
       expect(screen.getByText('Item 1')).toBeVisible();
     });
 
-    // Simulate item leaving viewport
-    const entry = {
-      isIntersecting: false,
-      boundingClientRect: { top: -50 } as DOMRectReadOnly,
-      target: { getAttribute: () => '0' } as unknown as Element, // data-index="0"
-      intersectionRatio: 0,
-      time: 0,
-      rootBounds: null,
-      intersectionRect: {} as DOMRectReadOnly,
-    } as IntersectionObserverEntry;
+    // Trigger scroll
+    const container = document.querySelector('.dashboard-main');
+    expect(container).not.toBeNull();
 
     act(() => {
-      // Trigger ALL registered observers
-      observerCallbacks.forEach(cb => cb([entry], {} as IntersectionObserver));
+      // Dispatch scroll event
+      fireEvent.scroll(container!);
     });
 
+    // Wait for throttle (500ms) + buffer
+    await new Promise(r => setTimeout(r, 600));
+
     await waitFor(() => {
       expect(global.fetch).toHaveBeenCalledWith(
         '/api/item/101',
-- 
cgit v1.2.3

