test_infinite_scroll.py raw
1 """Infinite scroll + Service Worker lifecycle tests.
2
3 Diagnoses:
4 1. SW registers and reaches "activated" state (not stuck in "parsed")
5 2. Initial feed subscription loads events
6 3. Scrolling to bottom triggers loadOlderFeed / "feed-more" subscription
7 4. Older events appear at the bottom of the feed
8 """
9
10 import json
11 import time
12 import uuid
13
14 import pytest
15 from selenium.webdriver.common.by import By
16 from selenium.webdriver.support.ui import WebDriverWait
17 from selenium.webdriver.support import expected_conditions as EC
18
19 from test.nostr_helpers import make_event, TEST_SECKEY, TEST_PUBKEY
20
21
22 def _publish_events(ws_url, count, start_time=None):
23 """Publish `count` kind-1 events, each 60s apart, oldest first."""
24 import websocket
25
26 if start_time is None:
27 start_time = int(time.time()) - count * 60
28
29 events = []
30 ws = websocket.create_connection(ws_url, timeout=10)
31 for i in range(count):
32 ts = start_time + i * 60
33 content = f"scroll-test-{i:03d}-{uuid.uuid4().hex[:6]}"
34 ev = make_event(TEST_SECKEY, content, kind=1, created_at=ts)
35 ws.send(json.dumps(["EVENT", ev]))
36 ok = json.loads(ws.recv())
37 assert ok[0] == "OK" and ok[2] is True, f"publish failed: {ok}"
38 events.append(ev)
39 ws.close()
40 return events
41
42
43 class TestServiceWorkerLifecycle:
44 """Verify the SW registers, installs, and activates without hanging."""
45
46 def test_sw_reaches_active(self, relay, browser, h):
47 """SW must reach 'activated' state within 10 seconds."""
48 browser.get(relay["url"])
49 time.sleep(2)
50
51 state = None
52 for _ in range(20):
53 state = h.js("""
54 if (!navigator.serviceWorker) return 'no-sw-support';
55 if (!navigator.serviceWorker.controller) return 'no-controller';
56 return navigator.serviceWorker.controller.state;
57 """)
58 if state == "activated":
59 break
60 time.sleep(0.5)
61
62 if state != "activated":
63 # Extra diagnostics.
64 reg_info = h.js("""
65 return navigator.serviceWorker.getRegistrations().then(regs => {
66 return regs.map(r => ({
67 scope: r.scope,
68 installing: r.installing ? r.installing.state : null,
69 waiting: r.waiting ? r.waiting.state : null,
70 active: r.active ? r.active.state : null,
71 }));
72 });
73 """)
74 pytest.fail(
75 f"SW state is '{state}' (expected 'activated') after 10s.\n"
76 f"Registrations: {json.dumps(reg_info, indent=2)}"
77 )
78
79 def test_sw_controls_page(self, relay, browser, h):
80 """After hard reload, SW must reclaim the page."""
81 browser.get(relay["url"])
82 time.sleep(3)
83
84 # Hard reload — clears cache, forces SW re-fetch.
85 h.js("location.reload()")
86 time.sleep(3)
87
88 controlled = h.js("return !!navigator.serviceWorker.controller")
89 assert controlled, "SW does not control page after reload"
90
91
92 class TestInfiniteScroll:
93 """Verify infinite scroll loads older events when scrolling to bottom."""
94
95 @pytest.fixture(autouse=True)
96 def seed_events(self, relay):
97 """Publish 40 events so we have enough for initial 20 + scroll batch."""
98 self.events = _publish_events(relay["ws"], 40)
99 yield
100
101 def test_initial_feed_loads(self, relay, browser, h):
102 """Feed should show events after setting pubkey."""
103 browser.get(relay["url"])
104 time.sleep(1)
105 h.js(f"localStorage.setItem('smesh-pubkey', '{TEST_PUBKEY}')")
106 browser.get(relay["url"])
107
108 # Wait for feed to render — up to 15s for SW + relay round-trip.
109 note_count = 0
110 for _ in range(15):
111 time.sleep(1)
112 note_count = h.js("""
113 var notes = document.querySelectorAll('[data-event-id]');
114 if (notes.length > 0) return notes.length;
115 // Fallback: count note-like divs.
116 var divs = document.querySelectorAll('div[style*="border-bottom"]');
117 return divs.length;
118 """) or 0
119 if note_count > 0:
120 break
121
122 assert note_count > 0, "No events rendered in feed"
123
124 def test_scroll_triggers_more(self, relay, browser, h):
125 """Scrolling to bottom should load older events."""
126 browser.get(relay["url"])
127 time.sleep(1)
128 h.js(f"localStorage.setItem('smesh-pubkey', '{TEST_PUBKEY}')")
129 browser.get(relay["url"])
130
131 # Wait for initial feed.
132 for _ in range(15):
133 time.sleep(1)
134 count = h.js("""
135 var divs = document.querySelectorAll('div[style*="border-bottom"]');
136 return divs.length;
137 """) or 0
138 if count >= 5:
139 break
140
141 initial_count = count
142 assert initial_count > 0, "No initial events loaded"
143
144 # Find the scrollable container and scroll to bottom.
145 scrolled = h.js("""
146 // Find the scrollable container (overflowY: auto).
147 var containers = document.querySelectorAll('div[style*="overflow"]');
148 for (var i = 0; i < containers.length; i++) {
149 var s = containers[i].style;
150 if (s.overflowY === 'auto' || s.overflow === 'auto') {
151 var el = containers[i];
152 el.scrollTop = el.scrollHeight;
153 return {
154 scrollTop: el.scrollTop,
155 scrollHeight: el.scrollHeight,
156 clientHeight: el.clientHeight,
157 found: true
158 };
159 }
160 }
161 return {found: false};
162 """)
163
164 assert scrolled and scrolled.get("found"), \
165 "Could not find scrollable container"
166
167 # Wait for more events to load (up to 10s).
168 final_count = initial_count
169 for _ in range(10):
170 time.sleep(1)
171 final_count = h.js("""
172 var divs = document.querySelectorAll('div[style*="border-bottom"]');
173 return divs.length;
174 """) or 0
175 if final_count > initial_count:
176 break
177
178 # Keep scrolling to bottom in case the container grew.
179 h.js("""
180 var containers = document.querySelectorAll('div[style*="overflow"]');
181 for (var i = 0; i < containers.length; i++) {
182 var s = containers[i].style;
183 if (s.overflowY === 'auto' || s.overflow === 'auto') {
184 containers[i].scrollTop = containers[i].scrollHeight;
185 break;
186 }
187 }
188 """)
189
190 assert final_count > initial_count, (
191 f"Infinite scroll did not load more events. "
192 f"Initial: {initial_count}, Final: {final_count}. "
193 f"Scroll info: {json.dumps(scrolled)}"
194 )
195
196 def test_scroll_debug_state(self, relay, browser, h):
197 """Diagnostic: dump the scroll handler's internal state."""
198 browser.get(relay["url"])
199 time.sleep(1)
200 h.js(f"localStorage.setItem('smesh-pubkey', '{TEST_PUBKEY}')")
201 browser.get(relay["url"])
202
203 # Wait for initial load.
204 for _ in range(15):
205 time.sleep(1)
206 count = h.js("""
207 var divs = document.querySelectorAll('div[style*="border-bottom"]');
208 return divs.length;
209 """) or 0
210 if count >= 5:
211 break
212
213 # Scroll to bottom and capture debug info.
214 debug = h.js("""
215 var result = {};
216
217 // Find scrollable container.
218 var containers = document.querySelectorAll('div[style*="overflow"]');
219 var el = null;
220 for (var i = 0; i < containers.length; i++) {
221 var s = containers[i].style;
222 if (s.overflowY === 'auto' || s.overflow === 'auto') {
223 el = containers[i];
224 break;
225 }
226 }
227 if (!el) return {error: 'no scrollable container found'};
228
229 result.beforeScroll = {
230 scrollTop: el.scrollTop,
231 clientHeight: el.clientHeight,
232 scrollHeight: el.scrollHeight,
233 };
234
235 // Scroll to bottom.
236 el.scrollTop = el.scrollHeight;
237
238 result.afterScroll = {
239 scrollTop: el.scrollTop,
240 clientHeight: el.clientHeight,
241 scrollHeight: el.scrollHeight,
242 };
243
244 // Check if the scroll event fires.
245 result.scrollFired = false;
246 el.addEventListener('scroll', function handler() {
247 result.scrollFired = true;
248 el.removeEventListener('scroll', handler);
249 });
250 el.scrollTop = el.scrollHeight; // trigger again
251
252 // Check SW state.
253 result.swController = !!navigator.serviceWorker.controller;
254 result.swState = navigator.serviceWorker.controller
255 ? navigator.serviceWorker.controller.state
256 : 'none';
257
258 // Count current notes.
259 result.noteCount = document.querySelectorAll(
260 'div[style*="border-bottom"]'
261 ).length;
262
263 return result;
264 """)
265
266 # Give the scroll event a moment.
267 time.sleep(0.5)
268
269 scroll_fired = h.js("""
270 var containers = document.querySelectorAll('div[style*="overflow"]');
271 for (var i = 0; i < containers.length; i++) {
272 var s = containers[i].style;
273 if (s.overflowY === 'auto' || s.overflow === 'auto') {
274 return containers[i].scrollTop;
275 }
276 }
277 return -1;
278 """)
279
280 print(f"\n=== SCROLL DEBUG ===")
281 print(f"Debug info: {json.dumps(debug, indent=2)}")
282 print(f"ScrollTop after wait: {scroll_fired}")
283 print(f"=== END DEBUG ===\n")
284
285 # This test always passes — it's diagnostic.
286 assert debug is not None, "Failed to collect debug info"
287