aboutsummaryrefslogtreecommitdiffstats
path: root/frontend/src
diff options
context:
space:
mode:
Diffstat (limited to 'frontend/src')
-rw-r--r--frontend/src/components/FeedItems.test.tsx33
-rw-r--r--frontend/src/components/FeedItems.tsx32
2 files changed, 42 insertions, 23 deletions
diff --git a/frontend/src/components/FeedItems.test.tsx b/frontend/src/components/FeedItems.test.tsx
index ca0dc98..6ffd026 100644
--- a/frontend/src/components/FeedItems.test.tsx
+++ b/frontend/src/components/FeedItems.test.tsx
@@ -137,13 +137,13 @@ describe('FeedItems Component', () => {
json: async () => mockItems,
});
- // Capture the callback
- let observerCallback: IntersectionObserverCallback = () => { };
+ // Capture both callbacks
+ const observerCallbacks: IntersectionObserverCallback[] = [];
- // Override the mock to capture callback
+ // Override the mock to capture both callbacks
class MockIntersectionObserver {
constructor(callback: IntersectionObserverCallback) {
- observerCallback = callback;
+ observerCallbacks.push(callback);
}
observe = vi.fn();
unobserve = vi.fn();
@@ -161,6 +161,11 @@ describe('FeedItems Component', () => {
expect(screen.getByText('Item 1')).toBeVisible();
});
+ // Wait for observers to be created
+ await waitFor(() => {
+ expect(observerCallbacks.length).toBeGreaterThan(0);
+ });
+
// Simulate item leaving viewport at the top
// Element index is 0
const entry = {
@@ -173,11 +178,10 @@ describe('FeedItems Component', () => {
intersectionRect: {} as DOMRectReadOnly,
} as IntersectionObserverEntry;
- // Use vi.waitUntil to wait for callback to be assigned if needed,
- // though strictly synchronous render + effect should do it.
- // Direct call:
+ // Call the last itemObserver (second-to-last in the array, since sentinelObserver is last)
act(() => {
- observerCallback([entry], {} as IntersectionObserver);
+ const lastItemObserver = observerCallbacks[observerCallbacks.length - 2];
+ lastItemObserver([entry], {} as IntersectionObserver);
});
await waitFor(() => {
@@ -199,10 +203,10 @@ describe('FeedItems Component', () => {
.mockResolvedValueOnce({ ok: true, json: async () => initialItems })
.mockResolvedValueOnce({ ok: true, json: async () => moreItems });
- let observerCallback: IntersectionObserverCallback = () => { };
+ const observerCallbacks: IntersectionObserverCallback[] = [];
class MockIntersectionObserver {
constructor(callback: IntersectionObserverCallback) {
- observerCallback = callback;
+ observerCallbacks.push(callback);
}
observe = vi.fn();
unobserve = vi.fn();
@@ -220,6 +224,11 @@ describe('FeedItems Component', () => {
expect(screen.getByText('Item 1')).toBeInTheDocument();
});
+ // Wait for observers to be created (effect runs multiple times)
+ await waitFor(() => {
+ expect(observerCallbacks.length).toBeGreaterThan(0);
+ });
+
// Simulate sentinel becoming visible
const entry = {
isIntersecting: true,
@@ -231,8 +240,10 @@ describe('FeedItems Component', () => {
intersectionRect: {} as DOMRectReadOnly,
} as IntersectionObserverEntry;
+ // Call the last sentinelObserver (second of the last pair created)
act(() => {
- observerCallback([entry], {} as IntersectionObserver);
+ const lastSentinelObserver = observerCallbacks[observerCallbacks.length - 1];
+ lastSentinelObserver([entry], {} as IntersectionObserver);
});
await waitFor(() => {
diff --git a/frontend/src/components/FeedItems.tsx b/frontend/src/components/FeedItems.tsx
index b497e9d..a058b70 100644
--- a/frontend/src/components/FeedItems.tsx
+++ b/frontend/src/components/FeedItems.tsx
@@ -164,17 +164,10 @@ export default function FeedItems() {
useEffect(() => {
- const observer = new IntersectionObserver(
+ // Observer for marking items as read
+ const itemObserver = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
- // Infinity scroll sentinel
- if (entry.target.id === 'load-more-sentinel') {
- if (entry.isIntersecting && !loadingMore && hasMore && items.length > 0) {
- fetchItems(String(items[items.length - 1]._id));
- }
- return;
- }
-
// If item is not intersecting and is above the viewport, it's been scrolled past
if (!entry.isIntersecting && entry.boundingClientRect.top < 0) {
const index = Number(entry.target.getAttribute('data-index'));
@@ -190,15 +183,30 @@ export default function FeedItems() {
{ root: null, threshold: 0 }
);
+ // Observer for infinite scroll (less aggressive, must be fully visible)
+ const sentinelObserver = new IntersectionObserver(
+ (entries) => {
+ entries.forEach((entry) => {
+ if (entry.isIntersecting && !loadingMore && hasMore && items.length > 0) {
+ fetchItems(String(items[items.length - 1]._id));
+ }
+ });
+ },
+ { root: null, threshold: 1.0 }
+ );
+
items.forEach((_, index) => {
const el = document.getElementById(`item-${index}`);
- if (el) observer.observe(el);
+ if (el) itemObserver.observe(el);
});
const sentinel = document.getElementById('load-more-sentinel');
- if (sentinel) observer.observe(sentinel);
+ if (sentinel) sentinelObserver.observe(sentinel);
- return () => observer.disconnect();
+ return () => {
+ itemObserver.disconnect();
+ sentinelObserver.disconnect();
+ };
}, [items, loadingMore, hasMore]);
if (loading) return <div className="feed-items-loading">Loading items...</div>;