diff options
| author | Adam Mathes <adam@adammathes.com> | 2026-02-16 19:17:59 -0800 |
|---|---|---|
| committer | Adam Mathes <adam@adammathes.com> | 2026-02-16 19:17:59 -0800 |
| commit | b500776f035779f9b9ee23ab889afa93ca987212 (patch) | |
| tree | 43b7edc592a4742969c5972c506fbc0f3feff438 /frontend-vanilla | |
| parent | e8a4f637dfa10510b350b95efaa4b5eb9a8f6f28 (diff) | |
| download | neko-b500776f035779f9b9ee23ab889afa93ca987212.tar.gz neko-b500776f035779f9b9ee23ab889afa93ca987212.tar.bz2 neko-b500776f035779f9b9ee23ab889afa93ca987212.zip | |
Update V2/V3 'mark as read' logic to require item bottom to be above viewport, while keeping V1 unchanged
Diffstat (limited to 'frontend-vanilla')
| -rw-r--r-- | frontend-vanilla/src/main.test.ts | 2 | ||||
| -rw-r--r-- | frontend-vanilla/src/main.ts | 4 | ||||
| -rw-r--r-- | frontend-vanilla/src/regression.test.ts | 59 |
3 files changed, 48 insertions, 17 deletions
diff --git a/frontend-vanilla/src/main.test.ts b/frontend-vanilla/src/main.test.ts index da2c41c..eb9e615 100644 --- a/frontend-vanilla/src/main.test.ts +++ b/frontend-vanilla/src/main.test.ts @@ -287,7 +287,7 @@ describe('main application logic', () => { if (itemEl) { itemEl.getBoundingClientRect = vi.fn(() => ({ - top: -50, bottom: 50, height: 100, left: 0, right: 0, width: 0, x: 0, y: 0, toJSON: () => { } + top: -150, bottom: -50, height: 100, left: 0, right: 0, width: 0, x: 0, y: 0, toJSON: () => { } })); } diff --git a/frontend-vanilla/src/main.ts b/frontend-vanilla/src/main.ts index a8606e3..02e4188 100644 --- a/frontend-vanilla/src/main.ts +++ b/frontend-vanilla/src/main.ts @@ -291,8 +291,8 @@ export function renderItems() { const el = document.querySelector(`.feed-item[data-id="${item._id}"]`); if (el) { const rect = el.getBoundingClientRect(); - // Mark as read if the top of the item is above the top of the container - if (rect.top < containerRect.top) { + // 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 }); } } diff --git a/frontend-vanilla/src/regression.test.ts b/frontend-vanilla/src/regression.test.ts index 94eb51e..a0b13d5 100644 --- a/frontend-vanilla/src/regression.test.ts +++ b/frontend-vanilla/src/regression.test.ts @@ -39,7 +39,7 @@ describe('Scroll-to-Read Regression Tests', () => { vi.useRealTimers(); }); - it('should mark item as read when existing in store and scrolled past', async () => { + it('should mark item as read when existing in store and fully scrolled past (bottom < container top)', async () => { vi.useRealTimers(); const mockItem = { _id: 999, @@ -51,16 +51,9 @@ describe('Scroll-to-Read Regression Tests', () => { store.setItems([mockItem]); - // Manually trigger the render logic that sets up the listener (implied by renderItems/init in real app) - // Here we can't easily trigger the exact closure in main.ts without fully initializing it. - // However, we can simulate the "check logic" if we extract it or replicate the test conditions - // that the previous main.test.ts used. - - // Since we are mocking the scroll behavior on the DOM element which main.ts attaches to: + // Manual setup const mainContent = document.getElementById('main-content'); expect(mainContent).not.toBeNull(); - - // We need to invoke renderItems to attach the listener renderItems(); // Mock getBoundingClientRect for container @@ -70,18 +63,18 @@ describe('Scroll-to-Read Regression Tests', () => { })); } - // Mock getBoundingClientRect for item (scrolled past top: -50) - // renderItems creates the DOM elements based on store + // Mock getBoundingClientRect for item (fully scrolled past: bottom < 0) + // Item top: -150, Item bottom: -50. Container Top: 0. + // -50 < 0, so should mark as read. const itemEl = document.querySelector(`.feed-item[data-id="999"]`); expect(itemEl).not.toBeNull(); if (itemEl) { itemEl.getBoundingClientRect = vi.fn(() => ({ - top: -50, bottom: 50, height: 100, left: 0, right: 0, width: 0, x: 0, y: 0, toJSON: () => { } + top: -150, bottom: -50, height: 100, left: 0, right: 0, width: 0, x: 0, y: 0, toJSON: () => { } })); } - // Prepare API mock vi.mocked(apiFetch).mockResolvedValue({ ok: true } as Response); // Trigger scroll event @@ -97,7 +90,45 @@ describe('Scroll-to-Read Regression Tests', () => { })); }); - it('should NOT mark item as read if NOT scrolled past', async () => { + it('should NOT mark item as read if only partially scrolled past (top < container top but bottom > container top)', async () => { + vi.useRealTimers(); + const mockItem = { + _id: 777, + title: 'Partial Test Item', + read: false, + url: 'http://example.com/partial', + publish_date: '2023-01-01' + } as any; + + store.setItems([mockItem]); + renderItems(); + + 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: () => { } + })); + } + + // Mock getBoundingClientRect for item (partially scrolled past: top < 0, bottom > 0) + // Item top: -50, Item bottom: 50. Container Top: 0. + // 50 is NOT < 0, so should NOT mark as read. + const itemEl = document.querySelector(`.feed-item[data-id="777"]`); + if (itemEl) { + itemEl.getBoundingClientRect = vi.fn(() => ({ + top: -50, bottom: 50, height: 100, left: 0, right: 0, width: 0, x: 0, y: 0, toJSON: () => { } + })); + } + + vi.mocked(apiFetch).mockResolvedValue({ ok: true } as Response); + + mainContent?.dispatchEvent(new Event('scroll')); + await new Promise(resolve => setTimeout(resolve, 300)); + + expect(apiFetch).not.toHaveBeenCalledWith(expect.stringContaining('/api/item/777'), expect.anything()); + }); + + it('should NOT mark item as read if NOT scrolled past (item below container top)', async () => { vi.useRealTimers(); const mockItem = { _id: 888, |
