diff options
Diffstat (limited to 'frontend-vanilla/src')
| -rw-r--r-- | frontend-vanilla/src/main.ts | 50 | ||||
| -rw-r--r-- | frontend-vanilla/src/regression.test.ts | 44 |
2 files changed, 65 insertions, 29 deletions
diff --git a/frontend-vanilla/src/main.ts b/frontend-vanilla/src/main.ts index aa00bd3..6a605c4 100644 --- a/frontend-vanilla/src/main.ts +++ b/frontend-vanilla/src/main.ts @@ -271,31 +271,17 @@ export function renderItems() { if (scrollRoot) { let readTimeoutId: number | null = null; scrollRoot.onscroll = () => { - // Infinite scroll: check immediately on every scroll event (cheap comparison). - // Guard: only when content actually overflows the container (scrollHeight > clientHeight). + // Infinite scroll check (container only) if (!store.loading && store.hasMore && scrollRoot.scrollHeight > scrollRoot.clientHeight) { if (scrollRoot.scrollHeight - scrollRoot.scrollTop - scrollRoot.clientHeight < 200) { loadMore(); } } - // Mark-as-read: debounced to avoid excessive DOM queries + // Mark-as-read: debounced if (readTimeoutId === null) { readTimeoutId = window.setTimeout(() => { - const containerRect = scrollRoot.getBoundingClientRect(); - - store.items.forEach((item) => { - if (item.read) return; - - const el = document.querySelector(`.feed-item[data-id="${item._id}"]`); - if (el) { - const rect = el.getBoundingClientRect(); - // Mark as read if the bottom of the item is above the top of the container - if (rect.bottom < containerRect.top) { - updateItem(item._id, { read: true }); - } - } - }); + checkReadItems(scrollRoot); readTimeoutId = null; }, 250); } @@ -303,6 +289,22 @@ export function renderItems() { } } +function checkReadItems(scrollRoot: HTMLElement) { + const containerRect = scrollRoot.getBoundingClientRect(); + store.items.forEach((item) => { + if (item.read) return; + + const el = document.querySelector(`.feed-item[data-id="${item._id}"]`); + if (el) { + const rect = el.getBoundingClientRect(); + // Mark as read if the bottom of the item is above the top of the container + if (rect.bottom < containerRect.top) { + updateItem(item._id, { read: true }); + } + } + }); +} + // 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. @@ -319,18 +321,8 @@ if (typeof window !== 'undefined') { 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 for read items periodically (robustness fallback) + checkReadItems(scrollRoot); // Check container scroll (if container is scrollable) if (scrollRoot.scrollHeight > scrollRoot.clientHeight) { diff --git a/frontend-vanilla/src/regression.test.ts b/frontend-vanilla/src/regression.test.ts index 813e4bb..0c10d95 100644 --- a/frontend-vanilla/src/regression.test.ts +++ b/frontend-vanilla/src/regression.test.ts @@ -167,6 +167,50 @@ describe('Scroll-to-Read Regression Tests', () => { // API should NOT be called expect(apiFetch).not.toHaveBeenCalledWith(expect.stringContaining('/api/item/888'), expect.anything()); }); + + it('should mark item as read when WINDOW scrolls (robustness fallback)', async () => { + vi.useRealTimers(); + const mockItem = { + _id: 12345, + title: 'Window Scroll Item', + read: false, + url: 'http://example.com/window', + publish_date: '2023-01-01' + } as any; + + store.setItems([mockItem]); + renderItems(); + + // Setup successful detection scenario + const mainContent = document.getElementById('main-content'); + if (mainContent) { + mainContent.getBoundingClientRect = vi.fn(() => ({ + top: 0, bottom: 800, height: 800, left: 0, right: 0, width: 0, x: 0, y: 0, toJSON: () => { } + })); + } + + const itemEl = document.querySelector(`.feed-item[data-id="12345"]`); + if (itemEl) { + // Fully scrolled past + itemEl.getBoundingClientRect = vi.fn(() => ({ + top: -150, bottom: -50, height: 100, left: 0, right: 0, width: 0, x: 0, y: 0, toJSON: () => { } + })); + } + + vi.mocked(apiFetch).mockResolvedValue({ ok: true } as Response); + + // Dispatch scroll on WINDOW, not mainContent + window.dispatchEvent(new Event('scroll')); + + // Wait for potential debounce/poll + await new Promise(resolve => setTimeout(resolve, 1100)); + + // Expect it to handle it + expect(apiFetch).toHaveBeenCalledWith(expect.stringContaining('/api/item/12345'), expect.objectContaining({ + method: 'PUT', + body: expect.stringContaining('"read":true') + })); + }); }); // NK-t8qnrh: Links in feed item descriptions should have no underlines (match v1 style) |
