#!/usr/bin/env python3 """Reels peeking: the next step plays as it slides in, and rewinds if it doesn't. Run against a locally running backend: backend/main.py & .venv/bin/python tests/smoke_peek.py A half-finished swipe is the thing under test, so the scroller's snapping is switched off for the duration: a finger holds the list between two steps, and `scrollTop` can only do the same with mandatory snapping out of the way. """ import sys from playwright.sync_api import sync_playwright BASE = "http://127.0.0.1:5000/" SERVER = "https://hottubapp.io" CHANNEL = "xvideos" SEED = """([server, channel]) => { localStorage.setItem('config', JSON.stringify({ servers: [{ [server]: {} }] })); localStorage.setItem('preferences', JSON.stringify({ [server]: { channelId: channel } })); localStorage.removeItem('session'); }""" # Every step in the window, by its index, with what its video is doing. STEPS = """() => { const out = {}; document.querySelectorAll('.feed-slide').forEach((slide) => { const video = slide.querySelector('.feed-video'); out[slide.dataset.step] = { active: slide.classList.contains('is-active'), loaded: !!slide.querySelector('.feed-pane.is-loaded'), paused: video ? video.paused : null, muted: video ? video.muted : null, t: video ? Math.round(video.currentTime * 10) / 10 : null, }; }); return { steps: out, activeIndex: App.state.feedActiveIndex }; }""" DRAG = """(fraction) => { const scroller = document.getElementById('feed-scroll'); // A finger can hold the list between two steps; scrollTop can only do the // same with mandatory snapping out of the way. scroller.style.scrollSnapType = 'none'; const height = scroller.clientHeight; scroller.scrollTop = Math.round(height * fraction); return scroller.scrollTop; }""" class Checks: def __init__(self): self.failed = 0 def ok(self, label, condition, detail=""): mark = "PASS" if condition else "FAIL" if not condition: self.failed += 1 print(f" [{mark}] {label}" + (f" -- {detail}" if detail and not condition else "")) def open_reels(page): page.goto(BASE, wait_until="domcontentloaded") page.evaluate(SEED, [SERVER, CHANNEL]) page.goto(BASE, wait_until="load") page.wait_for_selector(".video-card", timeout=90000) page.wait_for_timeout(1500) page.evaluate("() => App.feed.open()") page.wait_for_selector(".feed-slide", timeout=20000) page.wait_for_timeout(4000) def main(): c = Checks() with sync_playwright() as p: browser = p.chromium.launch(args=[ "--no-sandbox", "--disable-dev-shm-usage", "--disable-gpu", "--autoplay-policy=no-user-gesture-required", "--renderer-process-limit=1", "--js-flags=--max-old-space-size=512", ]) page = browser.new_page(viewport={"width": 430, "height": 930}) crashes = [] page.on("pageerror", lambda e: crashes.append(str(e))) open_reels(page) start = page.evaluate(STEPS) c.ok("the first step is playing", start["steps"]["0"]["paused"] is False, str(start)) print("\na swipe barely begun") page.evaluate(DRAG, 0.18) page.wait_for_timeout(1800) peek = page.evaluate(STEPS) nxt = peek["steps"].get("1") c.ok("the step sliding in is already playing", nxt and nxt["paused"] is False, str(peek)) c.ok("silently, so it doesn't talk over the one being left", nxt and nxt["muted"] is True, str(nxt)) c.ok("and the one being left plays on", peek["steps"]["0"]["paused"] is False, str(peek["steps"]["0"])) c.ok("without the swipe counting as done", peek["activeIndex"] == 0, str(peek["activeIndex"])) print("\nthinking better of it") page.evaluate(DRAG, 0) page.wait_for_timeout(1200) page.evaluate("() => App.feed._settlePeek()") page.wait_for_timeout(400) back = page.evaluate(STEPS) nxt = back["steps"].get("1") c.ok("the step that slid back in is stopped", nxt and nxt["paused"] is True, str(nxt)) c.ok("and wound back to the start", nxt and nxt["t"] == 0, str(nxt)) c.ok("the one being watched never stopped", back["steps"]["0"]["paused"] is False, str(back["steps"]["0"])) print("\nswiping again starts it from the top") page.evaluate(DRAG, 0.2) page.wait_for_timeout(1500) again = page.evaluate(STEPS)["steps"].get("1") c.ok("it plays again", again and again["paused"] is False, str(again)) c.ok("from the beginning, not from where the first peek got to", again and again["t"] is not None and again["t"] < 2.5, str(again)) print("\ncarrying the swipe through") page.evaluate(DRAG, 1) page.wait_for_timeout(2500) page.evaluate("() => App.feed._settlePeek()") page.wait_for_timeout(400) done = page.evaluate(STEPS) arrived = done["steps"].get("1") c.ok("the step arrived at is the active one", done["activeIndex"] == 1, str(done["activeIndex"])) c.ok("it is playing", arrived and arrived["paused"] is False, str(arrived)) c.ok("it was never wound back", arrived and arrived["t"] > 0, str(arrived)) c.ok("and it carries the feed's own sound setting", arrived and arrived["muted"] == page.evaluate("() => App.state.feedMuted !== false"), str(arrived)) c.ok("nothing threw along the way", not crashes, str(crashes[:2])) browser.close() print(f"\n{'OK' if not c.failed else 'FAILED'}: {c.failed} check(s) failed") return 1 if c.failed else 0 sys.exit(main())