#!/usr/bin/env python3 """Reels split-panel smoke tests. Run against a locally running backend: backend/main.py & .venv/bin/python tests/smoke_reels.py The load-bearing check is the preload guarantee: every panel must have its next video buffered before the reader swipes. With locked scrolling that is the same panel position in the next step, so it holds only while the next step is both built and preloaded for all of its panes -- which in turn rests on the floor of one step in windowBounds() and in setActive()'s preloadAhead. Both shrink as panes are added, and without the floor a wide split would leave panels with nothing to swipe to. """ 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'); }""" LAYOUT = """() => { const slide = document.querySelector('.feed-slide.is-active'); if (!slide) return null; const panes = Array.from(slide.querySelectorAll('.feed-pane')); const bounds = slide.getBoundingClientRect(); return { count: panes.length, reported: App.feed.paneCount(), videos: panes.map(p => p.dataset.videoId), // Controls positioned for a full viewport end up outside a short pane, // clipped by its overflow and unreachable. escaping: panes.reduce((bad, p, i) => { const pr = p.getBoundingClientRect(); ['.feed-fav-btn', '.feed-pip-btn', '.feed-format-btn', '.feed-pane-tools'] .forEach((sel) => { const el = p.querySelector(sel); if (!el || el.hidden) return; const r = el.getBoundingClientRect(); if (r.top < pr.top - 1 || r.bottom > pr.bottom + 1 || r.left < pr.left - 1 || r.right > pr.right + 1) bad.push(i + sel); }); return bad; }, []), within_slide: panes.every(p => { const r = p.getBoundingClientRect(); return r.top >= bounds.top - 1 && r.bottom <= bounds.bottom + 1; }), }; }""" PRELOAD = """() => { const active = App.state.feedActiveIndex; const rows = {}; document.querySelectorAll('.feed-slide').forEach((slide) => { const panes = Array.from(slide.querySelectorAll('.feed-pane')); rows[Number(slide.dataset.step) - active] = panes.map((p) => { const v = p.querySelector('.feed-video'); return !!(v && (v.getAttribute('src') || v._hlsPlayer)); }); }); return { per: App.feed.paneCount(), next: rows[1] || null, current: rows[0] || null }; }""" class Checks: def __init__(self): self.failed = 0 def ok(self, label, condition, detail=""): if not condition: self.failed += 1 print(f" [{'PASS' if condition else 'FAIL'}] {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") try: page.wait_for_selector(".video-card", timeout=90000) except Exception: page.goto(BASE, wait_until="load") page.wait_for_selector(".video-card", timeout=90000) page.wait_for_timeout(4000) page.evaluate("() => App.feed.toggle()") page.wait_for_selector(".feed-slide", timeout=20000) page.wait_for_timeout(3000) def split(page, selector): page.click(".feed-slide.is-active " + selector) 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", "--renderer-process-limit=1", ]) page = browser.new_page(viewport={"width": 1400, "height": 1000}) open_reels(page) print("\nsingle panel") one = page.evaluate(LAYOUT) c.ok("reels opens with one panel", one and one["count"] == 1, str(one)) print("\nsplit right, then split the new panel below") split(page, ".feed-pane .feed-pane-split-right") split(page, ".feed-pane:last-of-type .feed-pane-split-down") three = page.evaluate(LAYOUT) c.ok("three panels after two splits", three["count"] == 3, str(three["count"])) c.ok("paneCount agrees with the DOM", three["reported"] == three["count"]) c.ok("every panel shows a different video", len(set(three["videos"])) == len(three["videos"]), str(three["videos"])) c.ok("panels fit inside the slide", three["within_slide"]) c.ok("no control escapes its panel", not three["escaping"], str(three["escaping"])) print("\npreload") pre = page.evaluate(PRELOAD) c.ok("the next step exists", pre["next"] is not None) c.ok("every panel of the current step is loaded", pre["current"] and all(pre["current"]), str(pre["current"])) # The point of the exercise: nobody should swipe into an empty panel. c.ok("every panel has its next video preloaded", pre["next"] is not None and all(pre["next"]) and len(pre["next"]) == pre["per"], str(pre["next"])) print("\none swipe advances every panel") before = page.evaluate(LAYOUT)["videos"] page.evaluate("() => { const s = document.getElementById('feed-scroll');" " s.scrollTop += s.clientHeight; }") page.wait_for_timeout(3500) after = page.evaluate(LAYOUT)["videos"] c.ok("all panels moved on", all(v not in before for v in after if v), f"{before} -> {after}") c.ok("still preloaded after the swipe", all(page.evaluate(PRELOAD)["next"] or [False])) print("\nper-panel audio") page.click(".feed-slide.is-active .feed-pane:first-of-type .feed-pane-mute") page.wait_for_timeout(1200) page.evaluate("() => App.feed.renderSlides()") # re-activate the step page.wait_for_timeout(1500) muted = page.evaluate("""() => Array.from( document.querySelectorAll('.feed-slide.is-active .feed-pane .feed-video') ).map(v => v.muted)""") c.ok("only the unmuted panel has sound", muted and muted[0] is False and all(muted[1:]), str(muted)) print("\nclose a panel") page.click(".feed-slide.is-active .feed-pane .feed-pane-close") page.wait_for_timeout(3500) closed = page.evaluate(LAYOUT) c.ok("closing collapses the split", closed["count"] == 2, str(closed["count"])) c.ok("survivors still fit the slide", closed["within_slide"]) browser.close() print(f"\n{'FAILED' if c.failed else 'OK'}: {c.failed} check(s) failed") return 1 if c.failed else 0 if __name__ == "__main__": sys.exit(main())