- pill: two host dots -> ONE consolidated state (down > slow > up) — the at-a-glance question is 'can I watch?', which is one state; per-host detail belongs in the tap panel - panel: dashed slow-threshold line in the CDN sparkline (drawn at the server's real threshold, not a client copy) + origin up/down step sparkline so 'how long has the origin been down' is visible - server: origin probe keeps a 20-sample hist like the CDN probe; DEGRADED threshold is now ANIWEB_CDN_KBPS (default 40) and is returned in /api/cdn so the UI draws the line at the live value - tests: pill assertions switched to consolidated states; fixtures carry origin hist + threshold; assert both sparklines + threshold line - docs: README + SPEC synced; preview on :8322 restarted (pid 44382)
128 lines
5.1 KiB
Python
128 lines
5.1 KiB
Python
"""Hermetic browser tests for the header status pill + panel.
|
|
|
|
/api/cdn is mocked with page.route — the throwaway server's probe thread
|
|
runs against LIVE upstream, so its real data must never leak into these
|
|
assertions. Everything else (login, home) hits the throwaway server, which
|
|
never touches the real data. Skips if playwright is absent.
|
|
|
|
Pill states are CONSOLIDATED (one dot + one word): down (origin dead)
|
|
> slow (CDN degraded) > up. Per-host detail lives in the tap panel.
|
|
"""
|
|
import asyncio
|
|
import json
|
|
|
|
import pytest
|
|
|
|
async_playwright = pytest.importorskip("playwright.async_api").async_playwright
|
|
|
|
BASE = "http://127.0.0.1:8399"
|
|
LAUNCH_ARGS = ["--autoplay-policy=no-user-gesture-required"]
|
|
POLL = 100
|
|
|
|
|
|
def _cdn(state, kbps, origin="OK"):
|
|
# origin hist: all OK except the last 3 when origin is DOWN
|
|
ohist = [{"t": 1750000000 - 180 * (10 - i),
|
|
"state": "DOWN" if (origin == "DOWN" and i >= 8) else "OK"}
|
|
for i in range(11)]
|
|
return {
|
|
"state": state, "kbps": kbps, "ts": 1750000000, "threshold": 40,
|
|
"hist": [{"t": 1750000000 - 180 * (10 - i), "kbps": 20 + i * 10} for i in range(11)],
|
|
"origin": {"state": origin, "msg": "http 200", "ts": 1750000000, "hist": ohist},
|
|
}
|
|
|
|
|
|
async def _open(pw, body):
|
|
"""logged-in home page with /api/cdn mocked. alice has no history, so
|
|
home renders without touching the live upstream."""
|
|
browser = await pw.chromium.launch(args=LAUNCH_ARGS)
|
|
page = await (await browser.new_context()).new_page()
|
|
await page.route("**/api/cdn",
|
|
lambda r: r.fulfill(status=200, body=json.dumps(body),
|
|
headers={"Content-Type": "application/json"}))
|
|
await page.goto(BASE + "/")
|
|
await page.wait_for_selector("input#pw")
|
|
await page.fill("#user", "alice")
|
|
await page.fill("#pw", "alicepw12345")
|
|
await page.keyboard.press("Enter")
|
|
await page.wait_for_selector("#q", timeout=30000)
|
|
return browser, page
|
|
|
|
|
|
def _js_click(page, sel):
|
|
# JS click: this playwright/chromium combo's actionability check
|
|
# ("element stable") hangs non-deterministically on header elements
|
|
return page.evaluate(f"document.querySelector({json.dumps(sel)}).click()")
|
|
|
|
|
|
def test_pill_ok():
|
|
asyncio.run(_pill_ok())
|
|
|
|
|
|
async def _pill_ok():
|
|
async with async_playwright() as pw:
|
|
browser, page = await _open(pw, _cdn("OK", 120))
|
|
try:
|
|
await page.wait_for_function(
|
|
"document.querySelector('#pill .d').className.includes('ok')",
|
|
timeout=5000, polling=POLL)
|
|
assert await page.text_content("#pill span") == "up"
|
|
# tap opens the panel: two sparklines (CDN + origin), the CDN
|
|
# one carries the dashed threshold line and one point per sample
|
|
await _js_click(page, "#pill")
|
|
await page.wait_for_selector("#status-panel .sp-svg polyline")
|
|
assert await page.locator("#status-panel polyline").count() == 2
|
|
assert await page.locator("#status-panel .sp-thr").count() == 1
|
|
pts = await page.get_attribute("#status-panel .sp-svg polyline", "points")
|
|
assert len(pts.split()) == 11
|
|
assert "CDN: OK — 120 KB/s" in await page.text_content("#status-panel")
|
|
# tap again closes; so does a click away
|
|
await _js_click(page, "#pill")
|
|
assert not await page.locator("#status-panel").count()
|
|
await _js_click(page, "#pill")
|
|
await page.wait_for_selector("#status-panel")
|
|
await _js_click(page, "body")
|
|
assert not await page.locator("#status-panel").count()
|
|
finally:
|
|
await browser.close()
|
|
|
|
|
|
def test_pill_degraded():
|
|
asyncio.run(_pill_degraded())
|
|
|
|
|
|
async def _pill_degraded():
|
|
async with async_playwright() as pw:
|
|
browser, page = await _open(pw, _cdn("DEGRADED", 23))
|
|
try:
|
|
await page.wait_for_function(
|
|
"document.querySelector('#pill .d').className.includes('slow')",
|
|
timeout=5000, polling=POLL)
|
|
assert await page.text_content("#pill span") == "slow · 23 KB/s"
|
|
await _js_click(page, "#pill")
|
|
await page.wait_for_selector("#status-panel")
|
|
assert "CDN: DEGRADED — 23 KB/s" in await page.text_content("#status-panel")
|
|
finally:
|
|
await browser.close()
|
|
|
|
|
|
def test_pill_origin_down():
|
|
asyncio.run(_pill_origin_down())
|
|
|
|
|
|
async def _pill_origin_down():
|
|
async with async_playwright() as pw:
|
|
browser, page = await _open(pw, _cdn("OK", 120, origin="DOWN"))
|
|
try:
|
|
# down beats a healthy CDN: one consolidated state
|
|
await page.wait_for_function(
|
|
"document.querySelector('#pill .d').className.includes('down')",
|
|
timeout=5000, polling=POLL)
|
|
assert await page.text_content("#pill span") == "down"
|
|
assert "down" in await page.get_attribute("#pill", "class")
|
|
await _js_click(page, "#pill")
|
|
await page.wait_for_selector("#status-panel .sp-svg.bad")
|
|
assert "upstream: DOWN" in await page.text_content("#status-panel")
|
|
finally:
|
|
await browser.close()
|