#!/usr/bin/env python3 """Info panel smoke tests: what the panel shows, and what Copy JSON hands over. Run against a locally running backend: backend/main.py & .venv/bin/python tests/smoke_info.py The listing is served by this script so the expected JSON is known exactly -- including the awkward parts of a real item (nested http_headers, a tag array, a zero, an empty string). """ import json import os import sys from playwright.sync_api import sync_playwright BASE = os.environ.get("JACUZZI_BASE", "http://127.0.0.1:5000/") SERVER = "https://hottubapp.io" CHANNEL = "xvideos" ITEM = { "id": "test:copy-me", "title": "Summer Col and Damion Dayski", "url": "https://example.test/proxy/test/post/copy-me.html", "channel": "test", "duration": 0, "isLive": False, "views": 0, "uploader": "", "thumb": "https://cdn.example.test/thumb.png", "tags": ["sex", "bigtits", "bigass", "hardcore"], "preview": "https://example.test/proxy/test/post/copy-me.html", "http_headers": {"Referer": "https://example.test/"}, } SEED = """([server, channel]) => { localStorage.setItem('config', JSON.stringify({ servers: [{ [server]: {} }] })); localStorage.setItem('preferences', JSON.stringify({ [server]: { channelId: channel } })); localStorage.removeItem('session'); localStorage.setItem('favorites', JSON.stringify([])); }""" 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", # Lean flags, matching smoke_grid: this runs alongside the app's own # server and a default Chromium spikes hard enough at startup to get # itself killed on a constrained box. "--renderer-process-limit=1", "--js-flags=--max-old-space-size=512", ]) context = browser.new_context(viewport={"width": 1400, "height": 1000}) # The real clipboard, so the button's own path is what runs. context.grant_permissions(["clipboard-read", "clipboard-write"]) page = context.new_page() crashes = [] page.on("pageerror", lambda e: crashes.append(str(e))) page.route("**/api/videos", lambda route: route.fulfill( status=200, content_type="application/json", body=json.dumps({"items": [ITEM], "pageInfo": {"hasNextPage": False}}))) page.route("**/api/resolve*", lambda route: route.fulfill( status=200, content_type="application/json", body=json.dumps({"formats": [], "http_headers": {}, "isLive": False, "url": None}))) page.goto(BASE, wait_until="domcontentloaded") page.evaluate(SEED, [SERVER, CHANNEL]) page.goto(BASE, wait_until="load") page.wait_for_selector(".video-card", timeout=60000) page.wait_for_timeout(1500) print("\nthe panel opens with a copy button") page.evaluate("""() => { const v = App.state.loadedVideos.find((x) => x.id === 'test:copy-me'); App.ui.openInfo(v); }""") page.wait_for_timeout(500) c.ok("the panel is open", page.evaluate("() => document.getElementById('info-modal').classList.contains('open')")) c.ok("the button is visible", page.is_visible("#info-copy")) c.ok("and reads Copy JSON", page.inner_text("#info-copy").strip() == "Copy JSON", page.inner_text("#info-copy")) print("\nclicking it puts the item on the clipboard") page.click("#info-copy") page.wait_for_timeout(600) clipboard = page.evaluate("() => navigator.clipboard.readText()") try: copied = json.loads(clipboard) except ValueError: copied = None c.ok("what landed there is JSON", copied is not None, clipboard[:120]) if copied: missing = [k for k, v in ITEM.items() if k not in copied] c.ok("with every field the item has", not missing, str(missing)) c.ok("values intact, nested ones included", copied.get("http_headers") == ITEM["http_headers"] and copied.get("tags") == ITEM["tags"], str(copied.get("http_headers"))) c.ok("a zero is a zero, not a dash", copied.get("duration") == 0 and copied.get("views") == 0, str(copied.get("duration"))) c.ok("and an empty string stays empty", copied.get("uploader") == "", repr(copied.get("uploader"))) c.ok("it is pretty-printed, not one line", "\n" in clipboard) c.ok("the button says so", page.inner_text("#info-copy").strip() == "Copied", page.inner_text("#info-copy")) print("\nand it goes back to normal") page.wait_for_timeout(2000) c.ok("the label resets", page.inner_text("#info-copy").strip() == "Copy JSON", page.inner_text("#info-copy")) print("\nthe resolved payload rides along once it lands") page.evaluate("""() => { const v = App.state.loadedVideos.find((x) => x.id === 'test:copy-me'); App.ui.showInfo(v, { info: { extractorField: 'yes', formats: [] } }); }""") page.wait_for_timeout(300) page.click("#info-copy") page.wait_for_timeout(600) second = json.loads(page.evaluate("() => navigator.clipboard.readText()")) c.ok("the extractor's fields are in there too", (second.get("extractor") or {}).get("extractorField") == "yes", str(list(second.keys()))) c.ok("and the item's own are still there", second.get("id") == ITEM["id"]) 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())