diff options
Diffstat (limited to 'frontend-vanilla/src')
| -rw-r--r-- | frontend-vanilla/src/main.ts | 59 | ||||
| -rw-r--r-- | frontend-vanilla/src/polling.test.ts | 65 |
2 files changed, 123 insertions, 1 deletions
diff --git a/frontend-vanilla/src/main.ts b/frontend-vanilla/src/main.ts index 8d88470..aa00bd3 100644 --- a/frontend-vanilla/src/main.ts +++ b/frontend-vanilla/src/main.ts @@ -303,6 +303,60 @@ export function renderItems() { } } +// Polling fallback for infinite scroll (matches V1 behavior) +// This ensures that even if scroll events are missed or layout shifts occur without scroll, +// we still load more items when near the bottom. +if (typeof window !== 'undefined') { + setInterval(() => { + // We need to check if we are scrolling the window or an element. + // In V3 layout, .main-content handles the scroll if it's overflow-y: auto. + // But if .main-content is behaving like the body, we might need to check window.innerHeight. + + // Let's check the container first + const scrollRoot = document.getElementById('main-content'); + // console.log('Polling...', { scrollRoot: !!scrollRoot, loading: store.loading, hasMore: store.hasMore }); + + if (store.loading || !store.hasMore) return; + + if (scrollRoot) { + // DEBUG LOGGING + /* + console.log('Scroll Poll', { + scrollHeight: scrollRoot.scrollHeight, + scrollTop: scrollRoot.scrollTop, + clientHeight: scrollRoot.clientHeight, + offset: scrollRoot.scrollHeight - scrollRoot.scrollTop - scrollRoot.clientHeight, + docHeight: document.documentElement.scrollHeight, + winHeight: window.innerHeight, + winScroll: window.scrollY + }); + */ + + // Check container scroll (if container is scrollable) + if (scrollRoot.scrollHeight > scrollRoot.clientHeight) { + if (scrollRoot.scrollHeight - scrollRoot.scrollTop - scrollRoot.clientHeight < 200) { + loadMore(); + return; + } + } + } + + // Fallback: Check window scroll (if main-content isn't the scroller) + // This matches V1 logic: $(document).height() - $(window).height() - $(window).scrollTop() + const docHeight = document.documentElement.scrollHeight || document.body.scrollHeight; + const winHeight = window.innerHeight; + const winScroll = window.scrollY || document.documentElement.scrollTop; + + // Only if document is actually scrollable + if (docHeight > winHeight) { + if (docHeight - winHeight - winScroll < 200) { + loadMore(); + } + } + + }, 1000); +} + export function renderSettings() { const contentArea = document.getElementById('content-area'); if (!contentArea) return; @@ -613,7 +667,10 @@ export async function fetchItems(feedId?: string, tagName?: string, append: bool const res = await apiFetch(`/api/stream?${params.toString()}`); if (res.ok) { const items = await res.json(); - store.setHasMore(items.length >= 50); + // V1 logic: keep loading as long as we get results. + // Backend limit is currently 15, so checking >= 50 caused premature stop. + // We accept one extra empty fetch at the end to be robust against page size changes. + store.setHasMore(items.length > 0); store.setItems(items, append); } } finally { diff --git a/frontend-vanilla/src/polling.test.ts b/frontend-vanilla/src/polling.test.ts new file mode 100644 index 0000000..fa4b62f --- /dev/null +++ b/frontend-vanilla/src/polling.test.ts @@ -0,0 +1,65 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { store } from './store'; +import { apiFetch } from './api'; +import './main'; // Import to start the polling interval + +// Mock api +vi.mock('./api', () => ({ + apiFetch: vi.fn() +})); + +// Mock router to avoid errors during loadMore +vi.mock('./router', () => ({ + router: { + getCurrentRoute: () => ({ params: {}, query: new URLSearchParams() }), + updateQuery: vi.fn(), + navigate: vi.fn(), + addEventListener: vi.fn() + } +})); + +describe('Infinite Scroll Polling', () => { + beforeEach(() => { + // Use real timers because the interval starts at module import time + vi.useRealTimers(); + document.body.innerHTML = '<div id="main-content"></div>'; + store.setItems(Array(50).fill({ _id: 1 })); + store.setHasMore(true); + store.setLoading(false); + vi.clearAllMocks(); + }); + + it('should trigger loadMore via polling when near bottom', async () => { + const scrollRoot = document.getElementById('main-content')!; + + // Mock scroll properties + Object.defineProperty(scrollRoot, 'scrollHeight', { value: 2000, configurable: true }); + Object.defineProperty(scrollRoot, 'clientHeight', { value: 200, configurable: true }); + // Use defineProperty for scrollTop to ensure it overrides native behavior in JSDOM + Object.defineProperty(scrollRoot, 'scrollTop', { value: 1750, configurable: true }); + + // Mock apiFetch response + vi.mocked(apiFetch).mockResolvedValue({ + ok: true, + json: async () => [] + } as Response); + + // Wait for interval (1000ms) + buffer + await new Promise(resolve => setTimeout(resolve, 1100)); + + // Check if apiFetch was called + expect(apiFetch).toHaveBeenCalledWith(expect.stringContaining('/api/stream')); + }); + + it('should NOT trigger loadMore via polling when far from bottom', async () => { + const scrollRoot = document.getElementById('main-content')!; + + Object.defineProperty(scrollRoot, 'scrollHeight', { value: 2000, configurable: true }); + Object.defineProperty(scrollRoot, 'clientHeight', { value: 200, configurable: true }); + Object.defineProperty(scrollRoot, 'scrollTop', { value: 100, configurable: true }); + + await new Promise(resolve => setTimeout(resolve, 1100)); + + expect(apiFetch).not.toHaveBeenCalled(); + }); +}); |
