import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { store } from './store'; import { apiFetch } from './api'; import { updateItem, fetchItems, renderItems } from './main'; // Mock api vi.mock('./api', () => ({ apiFetch: vi.fn() })); // Mock main module functions that we aren't testing directly but are imported/used // Note: We need to use vi.importActual to get the real implementations we want to test // But main.ts has side effects and complex DOM manipulations, so we might want to mock parts of it. // Mock IntersectionObserver class MockIntersectionObserver { observe = vi.fn(); unobserve = vi.fn(); disconnect = vi.fn(); } vi.stubGlobal('IntersectionObserver', MockIntersectionObserver); describe('Scroll-to-Read Regression Tests', () => { beforeEach(() => { document.body.innerHTML = '
'; // Mock scrollIntoView Element.prototype.scrollIntoView = vi.fn(); vi.clearAllMocks(); store.setItems([]); // Setup default auth response vi.mocked(apiFetch).mockResolvedValue({ ok: true, status: 200, json: async () => [] } as Response); }); afterEach(() => { vi.useRealTimers(); }); it('should mark item as read when existing in store and scrolled past', async () => { vi.useRealTimers(); const mockItem = { _id: 999, title: 'Regression Test Item', read: false, url: 'http://example.com/regression', publish_date: '2023-01-01' } as any; 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: const mainContent = document.getElementById('main-content'); expect(mainContent).not.toBeNull(); // We need to invoke renderItems to attach the listener renderItems(); // Mock getBoundingClientRect for container 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 (scrolled past top: -50) // renderItems creates the DOM elements based on store 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: () => { } })); } // Prepare API mock vi.mocked(apiFetch).mockResolvedValue({ ok: true } as Response); // Trigger scroll event mainContent?.dispatchEvent(new Event('scroll')); // Wait for throttle (250ms) + buffer await new Promise(resolve => setTimeout(resolve, 300)); // Verify API call expect(apiFetch).toHaveBeenCalledWith(expect.stringContaining('/api/item/999'), expect.objectContaining({ method: 'PUT', body: expect.stringContaining('"read":true') })); }); it('should NOT mark item as read if NOT scrolled past', async () => { vi.useRealTimers(); const mockItem = { _id: 888, title: 'Visible Test Item', read: false, url: 'http://example.com/visible', publish_date: '2023-01-01' } as any; store.setItems([mockItem]); renderItems(); const mainContent = document.getElementById('main-content'); // Container if (mainContent) { mainContent.getBoundingClientRect = vi.fn(() => ({ top: 0, bottom: 800, height: 800, left: 0, right: 0, width: 0, x: 0, y: 0, toJSON: () => { } })); } // Item is still visible (top: 100) const itemEl = document.querySelector(`.feed-item[data-id="888"]`); if (itemEl) { itemEl.getBoundingClientRect = vi.fn(() => ({ top: 100, bottom: 200, 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)); // API should NOT be called expect(apiFetch).not.toHaveBeenCalledWith(expect.stringContaining('/api/item/888'), expect.anything()); }); });