aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/tests/harness
diff options
context:
space:
mode:
Diffstat (limited to 'packages/meshbay-hub/tests/harness')
-rwxr-xr-xpackages/meshbay-hub/tests/harness/photo_video_probe.py170
1 files changed, 170 insertions, 0 deletions
diff --git a/packages/meshbay-hub/tests/harness/photo_video_probe.py b/packages/meshbay-hub/tests/harness/photo_video_probe.py
new file mode 100755
index 0000000..02ebb90
--- /dev/null
+++ b/packages/meshbay-hub/tests/harness/photo_video_probe.py
@@ -0,0 +1,170 @@
+#!/usr/bin/env python3
+"""
+Videos among the photos of an album, in a real browser.
+
+Mounts the shipped `PhotosApp` on a made-up index of two photos and a phone's
+clip in one folder, with an `onPreview` that records what it was asked to
+play. Reads the album card, the clip's tile, the lightbox on the clip, and a
+slideshow started on it.
+
+ photo_video_probe.py
+
+Prints JSON: one entry per case.
+"""
+
+import http.server
+import json
+import socketserver
+import subprocess
+import sys
+import tempfile
+import threading
+import time
+from pathlib import Path
+
+STATIC = Path(__file__).resolve().parents[2] / "src" / "meshbay_hub" / "static"
+PORT = 8777
+RECORDS = []
+socketserver.TCPServer.allow_reuse_address = True
+
+FRAME = r"""<!doctype html><html><head><meta charset=utf-8>
+<link rel="stylesheet" href="/style.css"></head><body>
+<div id="photos"></div>
+<script type="module">
+import { html, render } from '/vendor/htm-preact.js';
+import { initLocale, setLocale } from '/i18n.js';
+import { PhotosApp } from '/photos-app.js';
+
+const LOGS = [];
+addEventListener('error', (e) => LOGS.push('error: ' + (e.message || e)));
+addEventListener('unhandledrejection',
+ (e) => LOGS.push('rejection: ' + (e.reason && e.reason.message || e.reason)));
+const frame = () => new Promise((r) => requestAnimationFrame(() => requestAnimationFrame(r)));
+const wait = (ms) => new Promise((r) => setTimeout(r, ms));
+
+const ENTRIES = [
+ { id: 'p1', name: 'a.jpg', path: 'Pics/2026-10', size: 4, type: 'image', added_at: 1 },
+ { id: 'v1', name: 'b.mp4', path: 'Pics/2026-10', size: 9e8, type: 'video', added_at: 2,
+ duration: 42 },
+ { id: 'p2', name: 'c.jpg', path: 'Pics/2026-10', size: 4, type: 'image', added_at: 3 },
+];
+const PLAYED = [];
+const ref = { current: null };
+
+(async () => {
+ const cases = [];
+ try {
+ setLocale('en');
+ await initLocale();
+ render(html`<${PhotosApp} groupId="g" transportRef=${ref} gekRef=${ref}
+ status="connected" entries=${ENTRIES} photoDirectories=${['Pics']}
+ setError=${() => {}} linkFor=${() => null}
+ onPreview=${(e) => PLAYED.push(e.id)} />`, document.getElementById('photos'));
+ await frame();
+ const card = document.querySelector('.photo-album-card');
+ cases.push({ case: 'card', text: card ? card.textContent : null });
+ card.click(); await frame();
+
+ const tiles = [...document.querySelectorAll('.photo-tile')];
+ cases.push({ case: 'tiles', count: tiles.length,
+ badges: tiles.map((t) => (t.querySelector('.photo-tile-video') || {}).textContent || null) });
+
+ tiles[1].click(); await frame();
+ const title = () => (document.querySelector('.photo-lightbox .video-title') || {}).textContent;
+ const play = document.querySelector('.photo-lightbox-play');
+ const onClip = title();
+ if (play) { play.click(); await frame(); }
+ cases.push({ case: 'lightbox', title: onClip, play: Boolean(play), played: PLAYED.slice(),
+ info: (document.querySelector('.photo-lightbox-info') || {}).textContent });
+
+ const show = [...document.querySelectorAll('.photo-lightbox .video-close')]
+ .find((b) => (b.title || '').toLowerCase().includes('slideshow'));
+ if (show) show.click();
+ await wait(300); await frame();
+ cases.push({ case: 'slideshow', button: Boolean(show), now: title() });
+
+ parent.postMessage({ cases, logs: LOGS, origin: location.origin }, '*');
+ } catch (err) {
+ parent.postMessage({ error: String(err && (err.stack || err)), logs: LOGS }, '*');
+ }
+})();
+</script></body></html>"""
+
+PAGE = r"""<!doctype html><html><head><meta charset=utf-8></head>
+<body style="margin:0"><div id="frames"></div><script>
+addEventListener('message', (e) => {
+ fetch('/log', { method: 'POST', body: JSON.stringify(e.data) });
+});
+const f = document.createElement('iframe');
+f.src = '/case';
+f.style.cssText = 'width:1100px;height:800px;border:0;display:block';
+document.getElementById('frames').appendChild(f);
+</script></body></html>"""
+
+
+class H(http.server.BaseHTTPRequestHandler):
+ def log_message(self, *a):
+ pass
+
+ def do_POST(self):
+ length = int(self.headers.get("Content-Length") or 0)
+ if self.path == "/log":
+ RECORDS.append(json.loads(self.rfile.read(length).decode()))
+ else:
+ self.rfile.read(length)
+ self.send_response(204)
+ self.end_headers()
+
+ def _send(self, body: bytes, ctype: str) -> None:
+ self.send_response(200)
+ self.send_header("Content-Type", ctype)
+ self.send_header("Content-Length", str(len(body)))
+ self.end_headers()
+ self.wfile.write(body)
+
+ def do_GET(self):
+ path = self.path.split("?")[0]
+ if path == "/":
+ self._send(PAGE.encode(), "text/html; charset=utf-8")
+ elif path == "/case":
+ self._send(FRAME.encode(), "text/html; charset=utf-8")
+ else:
+ asset = (STATIC / path.lstrip("/")).resolve()
+ if not str(asset).startswith(str(STATIC)) or not asset.is_file():
+ self.send_response(404)
+ self.end_headers()
+ return
+ self._send(asset.read_bytes(),
+ "text/css" if asset.suffix == ".css"
+ else "text/javascript" if asset.suffix == ".js"
+ else "application/octet-stream")
+
+
+def main() -> int:
+ with socketserver.TCPServer(("127.0.0.1", PORT), H) as srv:
+ threading.Thread(target=srv.serve_forever, daemon=True).start()
+ with tempfile.TemporaryDirectory(ignore_cleanup_errors=True) as profile:
+ proc = subprocess.Popen(
+ ["google-chrome", "--headless=new", "--disable-gpu", "--no-sandbox",
+ f"--user-data-dir={profile}", "--window-size=1100,900",
+ f"http://127.0.0.1:{PORT}/"],
+ stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
+ for _ in range(300):
+ if RECORDS:
+ break
+ time.sleep(0.1)
+ proc.terminate()
+ try:
+ proc.wait(timeout=10)
+ except subprocess.TimeoutExpired:
+ proc.kill()
+ proc.wait()
+ if not RECORDS:
+ print(json.dumps({"error": "no measurement"}), file=sys.stderr)
+ return 1
+ print(json.dumps(RECORDS[0], indent=1))
+ return 0
+
+
+if __name__ == "__main__":
+ raise SystemExit(main())