ui: loading states, player failure overlay + retry, toasts, back nav, full history page with delete

Bundle 1-4+7 from PLAN.md: skeleton loaders, player Loading/Buffering/fatal overlay with Retry, action toasts, back-to-home crumb + logo fix, ?history full-history view with per-entry delete (GET /api/history?all=1, DELETE /api/history). 14/14 tests green.
This commit is contained in:
jozamudi committed 2026-08-19 18:20:42 +00:00
1 parent 19e878800c
commit 2ec0f8a034
10 files changed
+414 -20

No files matched your search

+70 -2
View File
@@ -5,7 +5,10 @@ Covers what HTTP tests can't: the login UI, the user chip, and real
video playback behind auth. Needs: pip install playwright && playwright install chromium
"""
import asyncio
import http.cookiejar
import json
import sys
import urllib.request
from playwright.async_api import async_playwright
@@ -81,8 +84,8 @@ async def test_video_plays_behind_auth():
await _login(page, "alice", "alicepw12345")
await page.fill("#q", "chainsaw")
await page.keyboard.press("Enter")
await page.wait_for_selector(".card", timeout=30000)
await page.click(".card", force=True)
await page.wait_for_selector("a.card", timeout=30000)
await page.click("a.card", force=True)
await page.wait_for_selector("video", timeout=30000)
await page.wait_for_function(
"document.querySelector('video').readyState >= 4 && document.querySelector('video').currentTime > 1",
@@ -93,8 +96,73 @@ async def test_video_plays_behind_auth():
print("ok test_video_plays_behind_auth")
async def test_history_page_and_delete():
# self-contained: seed erin's own entries over HTTP first
jar = http.cookiejar.CookieJar()
op = urllib.request.build_opener(urllib.request.HTTPCookieProcessor(jar))
op.open(urllib.request.Request(BASE + "/login",
data=json.dumps({"username": "erin", "password": "erinpw12345"}).encode(),
headers={"Content-Type": "application/json"}, method="POST"))
for e in ({"title": "Hist A", "slug_id": "hist-a", "season_id": 1, "ep_id": 7,
"lang": "eng", "ep": 7, "pos": 5},
{"title": "Hist B", "slug_id": "hist-b", "season_id": 2, "ep_id": 9,
"lang": "jpn", "ep": 9, "pos": 30, "dur": 600}):
op.open(urllib.request.Request(BASE + "/api/history",
data=json.dumps(e).encode(),
headers={"Content-Type": "application/json"}, method="POST"))
async with async_playwright() as p:
browser = await p.chromium.launch(args=LAUNCH_ARGS)
page = await (await browser.new_context()).new_page()
errors = []
page.on("pageerror", lambda e: errors.append(str(e)))
await _login(page, "erin", "erinpw12345")
await page.goto(BASE + "/?history")
await page.wait_for_selector(".card.hist", timeout=15000)
await page.wait_for_function(
"document.querySelectorAll('.card.hist').length == 2", timeout=15000, polling=POLL)
await page.click(".card.hist .del", force=True)
await page.wait_for_function(
"document.querySelectorAll('.card.hist').length == 1", timeout=15000, polling=POLL)
await page.wait_for_selector(".toast", timeout=5000)
assert not errors, errors
await browser.close()
print("ok test_history_page_and_delete")
async def test_stream_failure_retry():
async with async_playwright() as p:
browser = await p.chromium.launch(args=LAUNCH_ARGS)
page = await (await browser.new_context()).new_page()
async def abort_stream(route):
await route.abort()
# note: **/stream/* (single star) — must NOT match the
# /api/episode/{id}/stream token endpoint, only the /stream/{token} proxy
await page.route("**/stream/*", abort_stream)
await _login(page, "admin", "adminpw12345")
await page.fill("#q", "chainsaw")
await page.keyboard.press("Enter")
await page.wait_for_selector("a.card", timeout=30000)
await page.click("a.card", force=True)
# detail auto-plays ep1; with /stream aborted hls.js exhausts its
# retries and surfaces the fatal error -> failure overlay with Retry
await page.wait_for_selector(".pl-overlay button", timeout=90000)
await page.unroute("**/stream/*", abort_stream)
await page.click(".pl-overlay button", force=True)
await page.wait_for_function(
"document.querySelector('video').readyState >= 4 && document.querySelector('video').currentTime > 1",
timeout=120000, polling=250)
await browser.close()
print("ok test_stream_failure_retry")
if __name__ == "__main__":
if len(sys.argv) > 1:
BASE = sys.argv[1]
asyncio.run(test_login_ui())
asyncio.run(test_video_plays_behind_auth())
asyncio.run(test_history_page_and_delete())
asyncio.run(test_stream_failure_retry())