"""Infinite scroll + Service Worker lifecycle tests. Diagnoses: 1. SW registers and reaches "activated" state (not stuck in "parsed") 2. Initial feed subscription loads events 3. Scrolling to bottom triggers loadOlderFeed / "feed-more" subscription 4. Older events appear at the bottom of the feed """ import json import time import uuid import pytest from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC from test.nostr_helpers import make_event, TEST_SECKEY, TEST_PUBKEY def _publish_events(ws_url, count, start_time=None): """Publish `count` kind-1 events, each 60s apart, oldest first.""" import websocket if start_time is None: start_time = int(time.time()) - count * 60 events = [] ws = websocket.create_connection(ws_url, timeout=10) for i in range(count): ts = start_time + i * 60 content = f"scroll-test-{i:03d}-{uuid.uuid4().hex[:6]}" ev = make_event(TEST_SECKEY, content, kind=1, created_at=ts) ws.send(json.dumps(["EVENT", ev])) ok = json.loads(ws.recv()) assert ok[0] == "OK" and ok[2] is True, f"publish failed: {ok}" events.append(ev) ws.close() return events class TestServiceWorkerLifecycle: """Verify the SW registers, installs, and activates without hanging.""" def test_sw_reaches_active(self, relay, browser, h): """SW must reach 'activated' state within 10 seconds.""" browser.get(relay["url"]) time.sleep(2) state = None for _ in range(20): state = h.js(""" if (!navigator.serviceWorker) return 'no-sw-support'; if (!navigator.serviceWorker.controller) return 'no-controller'; return navigator.serviceWorker.controller.state; """) if state == "activated": break time.sleep(0.5) if state != "activated": # Extra diagnostics. reg_info = h.js(""" return navigator.serviceWorker.getRegistrations().then(regs => { return regs.map(r => ({ scope: r.scope, installing: r.installing ? r.installing.state : null, waiting: r.waiting ? r.waiting.state : null, active: r.active ? r.active.state : null, })); }); """) pytest.fail( f"SW state is '{state}' (expected 'activated') after 10s.\n" f"Registrations: {json.dumps(reg_info, indent=2)}" ) def test_sw_controls_page(self, relay, browser, h): """After hard reload, SW must reclaim the page.""" browser.get(relay["url"]) time.sleep(3) # Hard reload — clears cache, forces SW re-fetch. h.js("location.reload()") time.sleep(3) controlled = h.js("return !!navigator.serviceWorker.controller") assert controlled, "SW does not control page after reload" class TestInfiniteScroll: """Verify infinite scroll loads older events when scrolling to bottom.""" @pytest.fixture(autouse=True) def seed_events(self, relay): """Publish 40 events so we have enough for initial 20 + scroll batch.""" self.events = _publish_events(relay["ws"], 40) yield def test_initial_feed_loads(self, relay, browser, h): """Feed should show events after setting pubkey.""" browser.get(relay["url"]) time.sleep(1) h.js(f"localStorage.setItem('smesh-pubkey', '{TEST_PUBKEY}')") browser.get(relay["url"]) # Wait for feed to render — up to 15s for SW + relay round-trip. note_count = 0 for _ in range(15): time.sleep(1) note_count = h.js(""" var notes = document.querySelectorAll('[data-event-id]'); if (notes.length > 0) return notes.length; // Fallback: count note-like divs. var divs = document.querySelectorAll('div[style*="border-bottom"]'); return divs.length; """) or 0 if note_count > 0: break assert note_count > 0, "No events rendered in feed" def test_scroll_triggers_more(self, relay, browser, h): """Scrolling to bottom should load older events.""" browser.get(relay["url"]) time.sleep(1) h.js(f"localStorage.setItem('smesh-pubkey', '{TEST_PUBKEY}')") browser.get(relay["url"]) # Wait for initial feed. for _ in range(15): time.sleep(1) count = h.js(""" var divs = document.querySelectorAll('div[style*="border-bottom"]'); return divs.length; """) or 0 if count >= 5: break initial_count = count assert initial_count > 0, "No initial events loaded" # Find the scrollable container and scroll to bottom. scrolled = h.js(""" // Find the scrollable container (overflowY: auto). var containers = document.querySelectorAll('div[style*="overflow"]'); for (var i = 0; i < containers.length; i++) { var s = containers[i].style; if (s.overflowY === 'auto' || s.overflow === 'auto') { var el = containers[i]; el.scrollTop = el.scrollHeight; return { scrollTop: el.scrollTop, scrollHeight: el.scrollHeight, clientHeight: el.clientHeight, found: true }; } } return {found: false}; """) assert scrolled and scrolled.get("found"), \ "Could not find scrollable container" # Wait for more events to load (up to 10s). final_count = initial_count for _ in range(10): time.sleep(1) final_count = h.js(""" var divs = document.querySelectorAll('div[style*="border-bottom"]'); return divs.length; """) or 0 if final_count > initial_count: break # Keep scrolling to bottom in case the container grew. h.js(""" var containers = document.querySelectorAll('div[style*="overflow"]'); for (var i = 0; i < containers.length; i++) { var s = containers[i].style; if (s.overflowY === 'auto' || s.overflow === 'auto') { containers[i].scrollTop = containers[i].scrollHeight; break; } } """) assert final_count > initial_count, ( f"Infinite scroll did not load more events. " f"Initial: {initial_count}, Final: {final_count}. " f"Scroll info: {json.dumps(scrolled)}" ) def test_scroll_debug_state(self, relay, browser, h): """Diagnostic: dump the scroll handler's internal state.""" browser.get(relay["url"]) time.sleep(1) h.js(f"localStorage.setItem('smesh-pubkey', '{TEST_PUBKEY}')") browser.get(relay["url"]) # Wait for initial load. for _ in range(15): time.sleep(1) count = h.js(""" var divs = document.querySelectorAll('div[style*="border-bottom"]'); return divs.length; """) or 0 if count >= 5: break # Scroll to bottom and capture debug info. debug = h.js(""" var result = {}; // Find scrollable container. var containers = document.querySelectorAll('div[style*="overflow"]'); var el = null; for (var i = 0; i < containers.length; i++) { var s = containers[i].style; if (s.overflowY === 'auto' || s.overflow === 'auto') { el = containers[i]; break; } } if (!el) return {error: 'no scrollable container found'}; result.beforeScroll = { scrollTop: el.scrollTop, clientHeight: el.clientHeight, scrollHeight: el.scrollHeight, }; // Scroll to bottom. el.scrollTop = el.scrollHeight; result.afterScroll = { scrollTop: el.scrollTop, clientHeight: el.clientHeight, scrollHeight: el.scrollHeight, }; // Check if the scroll event fires. result.scrollFired = false; el.addEventListener('scroll', function handler() { result.scrollFired = true; el.removeEventListener('scroll', handler); }); el.scrollTop = el.scrollHeight; // trigger again // Check SW state. result.swController = !!navigator.serviceWorker.controller; result.swState = navigator.serviceWorker.controller ? navigator.serviceWorker.controller.state : 'none'; // Count current notes. result.noteCount = document.querySelectorAll( 'div[style*="border-bottom"]' ).length; return result; """) # Give the scroll event a moment. time.sleep(0.5) scroll_fired = h.js(""" var containers = document.querySelectorAll('div[style*="overflow"]'); for (var i = 0; i < containers.length; i++) { var s = containers[i].style; if (s.overflowY === 'auto' || s.overflow === 'auto') { return containers[i].scrollTop; } } return -1; """) print(f"\n=== SCROLL DEBUG ===") print(f"Debug info: {json.dumps(debug, indent=2)}") print(f"ScrollTop after wait: {scroll_fired}") print(f"=== END DEBUG ===\n") # This test always passes — it's diagnostic. assert debug is not None, "Failed to collect debug info"