#!/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'', "", 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("", '
PENDING
", 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'
([^<]+)
', 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}")