v0.36.1
This commit is contained in:
1 parent
672c84bc13
commit
6f52ac3d00
38 files changed
+1183
-221
No files matched your search
@@ -0,0 +1,82 @@
|
||||
#!/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 => !node.matches(".chart-style-choice"));
|
||||
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: "network", "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}")
|
||||
@@ -0,0 +1,45 @@
|
||||
const assert = require("node:assert/strict");
|
||||
const fs = require("node:fs");
|
||||
const path = require("node:path");
|
||||
|
||||
const root = path.join(__dirname, "..");
|
||||
const read = relative => fs.readFileSync(path.join(root, relative), "utf8");
|
||||
const preferences = read("src/admin/web/app/preferences.js");
|
||||
const icons = read("src/admin/web/app/icons.js");
|
||||
const core = read("src/admin/web/app/core.js");
|
||||
const html = read("src/admin/web/index.html");
|
||||
const css = read("src/admin/web/css/10-base.css");
|
||||
const overviewCss = read("src/admin/web/css/30-overview.css");
|
||||
const integrations = read("src/admin/web/integrations.js");
|
||||
const metricsPartial = read("src/admin/web/partials/overview-metrics.html");
|
||||
const pl = JSON.parse(read("langs/pl_pl.json"));
|
||||
const openapi = JSON.parse(read("src/admin/openapi.json"));
|
||||
|
||||
assert.match(html, /id="ui-interface-icons"[^>]*type="checkbox"/);
|
||||
assert.match(preferences, /interface_icons:\s*false/);
|
||||
assert.match(preferences, /applyAdminInterfaceIcons\(value\.interface_icons\)/);
|
||||
assert.match(preferences, /saveAppearancePreference\(\{ interface_icons: next \}/);
|
||||
assert.match(preferences, /interface:\s*\{[^}]*interface_icons:\s*false/);
|
||||
assert.match(core, /rustProxyIcons\?\.refresh\?\.\(\)/);
|
||||
assert.match(icons, /dataset\.interfaceIcons !== "true"/);
|
||||
assert.match(icons, /aria-hidden", "true"/);
|
||||
assert.match(icons, /data\.panel|dataset\.panel/);
|
||||
assert.match(icons, /MutationObserver/);
|
||||
assert.match(icons, /if \(target\.dataset\.httpSettingsTab\) return "network"/);
|
||||
assert.match(icons, /#web-service-preset-apply.*return "wand"/);
|
||||
assert.match(icons, /#tunnels-open.*return null/);
|
||||
assert.match(icons, /#logs-menu > summary/);
|
||||
assert.match(integrations, /listener-h2-settings-link.*uiIconName = "settings"/);
|
||||
assert.match(integrations, /listener-h3-settings-link.*uiIconName = "settings"/);
|
||||
assert.match(metricsPartial, /stat-corner-badge[^>]+data-ui-icon-name="none"[^>]+id="tunnels-open"/);
|
||||
assert.match(overviewCss, /\.stat-corner-badge/);
|
||||
assert.equal(pl.translations.interfaceIconsHelp, "Dodaje semantyczne ikony SVG do nawigacji, akcji, zakładek i okien dialogowych.");
|
||||
assert.match(css, /html\[data-interface-icons="true"\]/);
|
||||
assert.equal(openapi.components.schemas.AdminPreferences.properties.interface_icons.default, false);
|
||||
assert.equal(openapi.components.schemas.AdminPreferences.properties.interface_icons.type, "boolean");
|
||||
|
||||
for (const icon of ["plus", "save", "check", "trash", "edit", "refresh", "settings", "search", "key", "certificate", "shield", "network", "database", "wand", "grid", "activity", "book"]) {
|
||||
assert.ok(new RegExp(`\\b${icon}:`).test(icons), `missing semantic SVG icon definition: ${icon}`);
|
||||
}
|
||||
|
||||
console.log("Panel SVG icon preference contract: OK");
|
||||
@@ -76,6 +76,13 @@ for path in sources:
|
||||
missing_refs = {dom_id: sorted(paths) for dom_id, paths in literal_refs.items() if dom_id not in id_set}
|
||||
assert not missing_refs, f"embedded panel JS references missing DOM ids: {missing_refs}"
|
||||
|
||||
assert "ui-interface-icons" in id_set, "Appearance must expose the SVG icon toggle"
|
||||
assert any(path.name == "icons.js" for path in sources), "semantic icon module must be embedded in app.js"
|
||||
|
||||
assert 'data-i18n="overviewExtraRecentSamples"' not in html, "Overview metric tiles must not show the redundant Recent samples footer"
|
||||
overview_extra = (WEB / "app/overview-extra-metrics.js").read_text(encoding="utf-8")
|
||||
assert 't("overviewExtraRecentSamples")' not in overview_extra, "Overview metric details must not fall back to the generic Recent samples label"
|
||||
|
||||
# Every visible primary navigation target must have a panel section. filelogs is
|
||||
# intentionally opened from the Logs menu rather than the main navigation.
|
||||
panel_targets = attribute_values(html, "data-panel")
|
||||
|
||||
@@ -11,6 +11,9 @@ const core = read("src/admin/web/app/core.js");
|
||||
const cache = read("src/admin/web/cache-panel.js");
|
||||
const transform = read("src/admin/web/transform-panel.js");
|
||||
const traffic = read("src/admin/web/traffic-panel.js");
|
||||
const refresh = read("src/admin/web/app/refresh.js");
|
||||
const management = read("src/admin/web/management.js");
|
||||
const loggingApi = read("src/admin/logging_api.rs");
|
||||
const openapi = JSON.parse(read("src/admin/openapi.json"));
|
||||
|
||||
assert.match(settings, /authenticated && name === "certificate-api"[^\n]+rustProxyCertificateApi\?\.load/);
|
||||
@@ -18,9 +21,18 @@ assert.match(settings, /authenticated && active === "certificate-api"[^\n]+rustP
|
||||
assert.ok(!/renderCertificateApi\(\);\s*loadCertificateApi\(\);\s*\}\)\(\);\s*$/.test(certificateApi),
|
||||
"Certificate API must not fetch credentials when its module is initialized");
|
||||
|
||||
assert.match(core, /async function refresh\(force = false, includeLogs = false\)/);
|
||||
assert.match(core, /if \(includeLogs\) tasks\.push\(rustProxyRefresh\.manual\("logs-fallback"\)\)/);
|
||||
assert.match(core, /refresh\(true, true\)\.catch/);
|
||||
assert.match(core, /async function refresh\(force = false\)/);
|
||||
assert.doesNotMatch(core, /includeLogs/);
|
||||
assert.match(core, /new EventSource\(`\/api\/events\?interval=\$\{seconds\}&logs=false`\)/);
|
||||
assert.match(loggingApi, /logs: Option<bool>/);
|
||||
assert.match(loggingApi, /let include_logs = query\.logs\.unwrap_or\(true\)/);
|
||||
assert.match(loggingApi, /if include_logs[\s\S]+logs_payload\(&state\)[\s\S]+else[\s\S]+live_status_payload/);
|
||||
assert.match(core, /if \(payload\.logs\) renderLogs\(payload\.logs\)/);
|
||||
|
||||
assert.match(refresh, /active: \(\) => !!window\.rustProxyLogs\?\.active\?\.\(\)/);
|
||||
assert.doesNotMatch(refresh, /manual\("logs-fallback"\).*manual\("system-info"\)/);
|
||||
assert.match(management, /function active\(\) \{ return dialog\.open && !paused && !expanded; \}/);
|
||||
assert.match(management, /syncLivePolling\(true\)/);
|
||||
|
||||
for (const [name, source, endpoint] of [
|
||||
["cache", cache, "/api/cache"],
|
||||
@@ -35,5 +47,9 @@ for (const [name, source, endpoint] of [
|
||||
assert.deepEqual(view?.schema?.enum, ["live"], `${endpoint} must document view=live`);
|
||||
}
|
||||
|
||||
assert.equal(openapi.info.version, "0.36.0");
|
||||
const events = openapi.paths["/api/events"].get;
|
||||
const logsFlag = (events.parameters || []).find(parameter => parameter.name === "logs");
|
||||
assert.equal(logsFlag?.schema?.type, "boolean");
|
||||
assert.equal(logsFlag?.schema?.default, true);
|
||||
assert.equal(openapi.info.version, "0.36.1");
|
||||
console.log("Panel refresh/API contract checks: OK");
|
||||
Reference in new issue
Block a user