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