Files
RustProxy/tests/panel_icons_browser.py
T
2026-11-07 15:26:06 +01:00

83 lines
5.1 KiB
Python

#!/usr/bin/env python3
"""Browser smoke test for the optional semantic SVG interface icons.
Uses a locally installed Chromium when available and otherwise exits successfully
with a skip message. It exercises the real index.html plus the embedded icon
module without requiring a running RustProxy backend.
"""
from __future__ import annotations
from pathlib import Path
import html
import re
import shutil
import subprocess
import tempfile
ROOT = Path(__file__).resolve().parents[1]
WEB = ROOT / "src/admin/web"
chromium = next((shutil.which(name) for name in ("chromium", "chromium-browser", "google-chrome") if shutil.which(name)), None)
if not chromium:
print("Panel SVG icon browser smoke: SKIP (Chromium not installed)")
raise SystemExit(0)
page = (WEB / "index.html").read_text(encoding="utf-8")
page = re.sub(r'<script\s+defer=""\s+src="/assets/app___RUSTPROXY_ASSET_ID__\.js"></script>', "", page, count=1)
icons = (WEB / "app/icons.js").read_text(encoding="utf-8")
script = r'''
const $ = id => document.getElementById(id);
const translations = { edit: "Edit", remove: "Remove", copyValue: "Copy value", previous: "Previous", next: "Next" };
function t(key) { return translations[key] || key; }
''' + icons + r'''
(() => {
const fail = message => { document.getElementById("browser-icon-test").textContent = `FAIL: ${message}`; throw new Error(message); };
const result = document.getElementById("browser-icon-test");
const eligible = [...document.querySelectorAll("button, a.primary, a.outline, a.quiet")].filter(node => window.rustProxyIcons.iconFor(node) !== null);
if (document.querySelectorAll("svg.ui-icon").length !== 0) fail("icons must be absent before enabling the preference");
window.rustProxyIcons.setEnabled(true);
const decorated = eligible.filter(node => node.querySelector(":scope > svg.ui-icon"));
if (decorated.length !== eligible.length) fail(`expected ${eligible.length} decorated controls, got ${decorated.length}`);
if ([...document.querySelectorAll("svg.ui-icon")].some(icon => icon.getAttribute("aria-hidden") !== "true")) fail("SVG icons must be aria-hidden");
const expectedPanels = { overview: "grid", vhosts: "network", "web-server": "globe", "docker-vhosts": "box", redirects: "route", notfound: "fileX", streams: "transfer", "acl-profiles": "shield", tls: "certificate", traffic: "activity", cache: "database", transform: "wand", settings: "settings" };
for (const [panel, icon] of Object.entries(expectedPanels)) {
const button = document.querySelector(`[data-panel="${panel}"]`);
if (button?.querySelector(":scope > svg.ui-icon")?.dataset.uiIcon !== icon) fail(`panel ${panel} must use ${icon}`);
}
const dynamic = document.createElement("button"); dynamic.type = "button"; dynamic.className = "quiet"; dynamic.textContent = "Edit"; document.body.append(dynamic);
window.rustProxyIcons.refresh(dynamic);
if (dynamic.querySelector("svg.ui-icon")?.dataset.uiIcon !== "edit") fail("translated dynamic Edit action must use edit icon");
dynamic.textContent = "Remove item"; dynamic.setAttribute("aria-label", "Remove item"); window.rustProxyIcons.refresh(dynamic);
if (dynamic.querySelector("svg.ui-icon")?.dataset.uiIcon !== "trash") fail("dynamic Remove action must use trash icon");
const visibility = document.createElement("button"); visibility.dataset.dashboardWidgetAction = "toggle"; visibility.className = "dashboard-widget-toggle is-hidden"; document.body.append(visibility);
window.rustProxyIcons.refresh(visibility);
if (visibility.querySelector("svg.ui-icon")?.dataset.uiIcon !== "eyeOff") fail("hidden widget action must use eyeOff");
visibility.classList.remove("is-hidden"); window.rustProxyIcons.refresh(visibility);
if (visibility.querySelector("svg.ui-icon")?.dataset.uiIcon !== "eye") fail("visible widget action must use eye");
window.rustProxyIcons.setEnabled(false);
if (document.querySelectorAll("svg.ui-icon").length !== 0) fail("disabling preference must remove all injected icons");
result.textContent = `OK ${eligible.length} controls`;
})();
'''
page = page.replace("</body>", '<pre id="browser-icon-test">PENDING</pre><script>' + script + "</script></body>", 1)
with tempfile.TemporaryDirectory(prefix="rustproxy-icons-") as tmp:
target = Path(tmp) / "icons-test.html"
target.write_text(page, encoding="utf-8")
try:
proc = subprocess.run(
[chromium, "--headless", "--no-sandbox", "--disable-gpu", "--disable-dev-shm-usage", "--virtual-time-budget=1000", "--dump-dom", target.as_uri()],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
timeout=8,
check=False,
)
except subprocess.TimeoutExpired:
print("Panel SVG icon browser smoke: SKIP (Chromium cannot start in this environment)")
raise SystemExit(0)
match = re.search(r'<pre id="browser-icon-test">([^<]+)</pre>', proc.stdout)
status = html.unescape(match.group(1)) if match else "missing result marker"
if proc.returncode != 0 or not status.startswith("OK "):
raise SystemExit(f"Panel SVG icon browser smoke failed: {status}\n{proc.stderr[-4000:]}")
print(f"Panel SVG icon browser smoke: {status}")