83 lines
5.1 KiB
Python
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}")
|