Files
jacuzzi/tests/smoke_hud.py
Simon 741786c7cf Leave the controls down while swiping through reels
Scrolling woke the HUD, so a swipe to the next reel put the controls up
over the video arriving -- the one thing the swipe was for -- and left them
there for a second. The scroll now puts them away instead.

Removing that alone wasn't enough: the container also woke on pointerdown
and touchstart, which is how every drag begins, so the HUD still flashed at
the start of one. A press only wakes them now if it lands on a control --
which is a press on something the reader can't see while they're hidden,
and never the start of a swipe. A press that turns out to be a tap still
wakes them through the gesture layer's onSingleTap, the mouse moving still
wakes them, and dragging the scrubber keeps them up, being the one drag
that is the HUD.

Two details follow from it. The sleep sits after onScroll's suppressScroll
guard, so a resize or a picture-in-picture re-snap doesn't count as the
reader moving. And opening the feed scrolls to the starting step, whose
scroll event would put the controls straight back down, so that wake waits
a frame -- scroll events run before animation frames.

The checks live in their own file rather than in smoke_reels: one panel and
one swipe is all they need, and the split-panel suite is the heaviest thing
in the tree to run.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MPZFnNdHbPGDTqQUNiE4ZN
2026-09-21 17:57:04 +00:00

125 lines
4.5 KiB
Python

#!/usr/bin/env python3
"""The reels HUD: up when you're using it, down when you're swiping past it.
Run against a locally running backend:
backend/main.py &
.venv/bin/python tests/smoke_hud.py
One panel and one swipe, deliberately: what's under test is when the controls
come and go, not anything about the layout.
The presses here are dispatched rather than clicked, because moving a mouse is
itself a reason to wake the HUD and would hide what is being asked.
"""
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');
}"""
IDLE = """() => document.getElementById('feed-view').classList.contains('feed-hud-idle')"""
PRESS = """(selector) => {
const el = document.querySelector('.feed-slide.is-active ' + selector);
if (!el) return 'missing';
el.dispatchEvent(new PointerEvent('pointerdown', { bubbles: true }));
return document.getElementById('feed-view').classList.contains('feed-hud-idle');
}"""
SWIPE = """() => {
const s = document.getElementById('feed-scroll');
s.scrollTop += s.clientHeight;
}"""
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 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)))
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:
# One reload, in case the status refresh or the listing failed.
page.goto(BASE, wait_until="load")
page.wait_for_selector(".video-card", timeout=90000)
page.wait_for_timeout(2500)
page.evaluate("() => App.feed.open()")
page.wait_for_selector(".feed-slide", timeout=20000)
page.wait_for_timeout(400)
print("\nopening the feed")
c.ok("the controls are up to start with", page.evaluate(IDLE) is False)
print("\nleaving it alone")
page.wait_for_timeout(1600)
c.ok("they fade on their own", page.evaluate(IDLE) is True)
print("\nswiping to the next reel")
page.evaluate(SWIPE)
page.wait_for_timeout(300)
c.ok("the swipe does not put them back up mid-flight", page.evaluate(IDLE) is True)
page.wait_for_timeout(2500)
c.ok("nor once it has landed", page.evaluate(IDLE) is True)
c.ok("and the reel it landed on is playing",
page.evaluate("""() => {
const v = document.querySelector('.feed-slide.is-active .feed-video');
return !!v && !v.paused;
}"""))
print("\nwhat does bring them back")
c.ok("pressing the video -- which is how a swipe begins -- does not",
page.evaluate(PRESS, ".feed-video") is True)
c.ok("pressing a control does", page.evaluate(PRESS, ".feed-pane-mute") is False)
page.wait_for_timeout(1600)
c.ok("after which they fade again", page.evaluate(IDLE) is True)
page.mouse.move(200, 400)
page.mouse.move(210, 420)
page.wait_for_timeout(200)
c.ok("moving the mouse does too", page.evaluate(IDLE) is False)
print("\na swipe started while they are up")
c.ok("puts them away", (page.evaluate(SWIPE), page.wait_for_timeout(500),
page.evaluate(IDLE))[2] is True)
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())