136 lines
8.9 KiB
JavaScript
136 lines
8.9 KiB
JavaScript
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, /vhost-quick-toggle\.is-enable.*return "toggleOn"/);
|
|
assert.match(icons, /vhost-quick-toggle\.is-disable.*return "toggleOff"/);
|
|
assert.equal((html.match(/class="outline about-repository" data-ui-icon-name="none"/g) || []).length, 4);
|
|
assert.match(metricsPartial, /backend-latency-stat[\s\S]*backend-health-origins[\s\S]*backend-dashboard-latency-detail/);
|
|
assert.equal(JSON.parse(read("langs/en_US.json")).translations.backendWidgetProbeCount, "Health Origins");
|
|
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", "toggleOn", "toggleOff"]) {
|
|
assert.ok(new RegExp(`\\b${icon}:`).test(icons), `missing semantic SVG icon definition: ${icon}`);
|
|
}
|
|
|
|
// 0.36.2 semantic review: verbs/explicit ids must win over colliding keywords.
|
|
const explicitIcons = { "overview-layout-unlock": "unlock", "overview-inline-save": "lock", "overview-layout-open": "layers", "docker-settings-open": "settings", "login-submit": "login", "revoke-sessions": "logout", "about-open": "info" };
|
|
for (const [id, icon] of Object.entries(explicitIcons)) assert.match(icons, new RegExp(`"${id}": "${icon}"`), `${id} must use ${icon}`);
|
|
assert.match(icons, /\(\^\|\[\^p\]\)reset/, "preset buttons must not match the reset rule");
|
|
assert.match(icons, /UI_GLYPH_LABEL/);
|
|
assert.match(icons, /raw === key/, "English labels equal to their key must still count as translated");
|
|
for (const icon of ["unlock", "layers", "expand", "collapse", "ban", "eraser", "hash", "info", "plug", "transfer", "mapPin", "compare"]) {
|
|
assert.ok(new RegExp(`\\b${icon}:`).test(icons), `missing semantic SVG icon definition: ${icon}`);
|
|
}
|
|
|
|
const logo = read("src/admin/web/app/logo-animation.js");
|
|
const router = read("src/admin/router.rs");
|
|
const shellCss = read("src/admin/web/css/20-shell.css");
|
|
assert.match(html, /id="ui-logo-animation"[^>]*type="checkbox"/);
|
|
assert.match(router, /web\/app\/logo-animation\.js/);
|
|
assert.match(preferences, /logo_animation:\s*false/);
|
|
assert.match(preferences, /applyAdminLogoAnimation\(value\.logo_animation\)/);
|
|
assert.match(preferences, /saveAppearancePreference\(\{ logo_animation: next \}/);
|
|
assert.match(logo, /prefers-reduced-motion: reduce/);
|
|
assert.match(logo, /document\.hidden/);
|
|
assert.match(shellCss, /prefers-reduced-motion: reduce[\s\S]*data-logo-effect/);
|
|
assert.equal(openapi.components.schemas.AdminPreferences.properties.logo_animation.default, false);
|
|
assert.ok(pl.translations.logoAnimation && pl.translations.logoAnimationHelp);
|
|
|
|
// Mobile/Safari regression: injected icons must never be mistaken for chart SVGs or
|
|
// stretched by descendant chart rules, and refreshes must not rebuild decorated buttons.
|
|
const runtimeJs = read("src/admin/web/app/runtime.js");
|
|
const scalesJs = read("src/admin/web/app/chart-scales.js");
|
|
const editorsCss = read("src/admin/web/css/40-editors.css");
|
|
assert.match(runtimeJs, /row\.querySelector\("svg:not\(\.ui-icon\)"\)/);
|
|
assert.doesNotMatch(runtimeJs, /row\.querySelector\("svg"\)/);
|
|
assert.match(runtimeJs, /querySelector\(":scope > svg\.ui-icon"\)/);
|
|
assert.match(scalesJs, /svg\.classList\.contains\("ui-icon"\)/);
|
|
assert.match(scalesJs, /querySelectorAll\("svg:not\(\.ui-icon\)"\)/);
|
|
for (const source of [css, overviewCss, editorsCss]) assert.doesNotMatch(source, /(^|\n)[^\n{}]*(\.network-interface-row|\.stat-chart|\.activity-card|\.system-card|\.function-live-chart|\.mini-live-chart|\.vhost-chart|\.connection-card) svg[ ,{]/, "descendant chart svg rules must exclude .ui-icon");
|
|
assert.match(css, /html\[data-interface-icons="true"\] :is\(button, a, summary\)>svg\.ui-icon/);
|
|
assert.match(icons, /\.mobile-menu-toggle"\) return null|draft-dock-toggle, \.mobile-menu-toggle/);
|
|
|
|
assert.match(editorsCss, /Spacing review \(0\.36\.2\)/);
|
|
assert.match(editorsCss, /\.vhost-backend-protocol-badge \{[^}]*letter-spacing: 1px;[^}]*\}/);
|
|
assert.match(editorsCss, /\.tls-pqc-control>a/);
|
|
|
|
console.log("Panel SVG icon preference contract: OK");
|
|
|
|
// CSS cleanup (0.36.3): the legacy body[data-density] mechanism must not come back.
|
|
{
|
|
const cssDir = path.join(__dirname, "..", "src", "admin", "web", "css");
|
|
for (const name of fs.readdirSync(cssDir).filter((file) => file.endsWith(".css"))) {
|
|
assert.doesNotMatch(fs.readFileSync(path.join(cssDir, name), "utf8"), /body\[data-density/, `${name} still has legacy body[data-density] rules`);
|
|
}
|
|
}
|
|
|
|
// Cache outcomes bar shows percentages inside its segments and hides labels that do not fit.
|
|
{
|
|
const rt = fs.readFileSync(path.join(__dirname, "..", "src", "admin", "web", "app", "runtime.js"), "utf8");
|
|
assert.match(rt, /function fitCacheMixLabels/);
|
|
assert.match(rt, /segment\.textContent = mix && pct > 0/);
|
|
}
|
|
|
|
// Settings layout (0.36.3): rows pattern for Appearance and horizontal sub-tabs.
|
|
{
|
|
const css = fs.readFileSync(path.join(__dirname, "..", "src", "admin", "web", "css", "50-settings.css"), "utf8");
|
|
assert.match(css, /#settings-page-appearance \.appearance-settings-grid>label \{/);
|
|
assert.match(css, /#panel-settings \.settings-tabs\.sub-tabs \{\s*display: flex;\s*flex-direction: row;/);
|
|
}
|
|
|
|
// Chart styles (0.36.3): every style id must be wired through Rust validation, OpenAPI, JS, CSS tokens, the selector and the language pack.
|
|
{
|
|
const root = path.join(__dirname, "..");
|
|
const read = (file) => fs.readFileSync(path.join(root, file), "utf8");
|
|
const styles = ["default", "soft", "technical", "high_contrast", "neon", "monochrome", "warm", "cool", "sunset", "aurora", "ocean", "forest", "pastel", "terminal", "blueprint", "colorblind"];
|
|
const rust = read("src/admin/preferences.rs"), js = read("src/admin/web/app/preferences.js"), tokens = read("src/admin/web/css/00-tokens.css"), html = read("src/admin/web/index.html");
|
|
const openapi = JSON.parse(read("src/admin/openapi.json")).components.schemas;
|
|
const enumValues = JSON.stringify(openapi).match(/"chart_style":\{"type":"string","enum":(\[[^\]]*\])/);
|
|
assert.ok(enumValues, "chart_style enum must be present in OpenAPI");
|
|
const documented = JSON.parse(enumValues[1]);
|
|
for (const style of styles) {
|
|
assert.ok(documented.includes(style), `OpenAPI chart_style missing ${style}`);
|
|
assert.ok(rust.includes(`"${style}"`), `Rust validation missing ${style}`);
|
|
assert.ok(js.includes(`"${style}"`), `preferences.js missing ${style}`);
|
|
assert.ok(tokens.includes(`html[data-chart-style=${style}]`), `tokens missing ${style}`);
|
|
assert.ok(tokens.includes(`.chart-style-choice[data-chart-preset=${style}]`), `swatches missing ${style}`);
|
|
assert.ok(html.includes(`value="${style}"`), `selector missing ${style}`);
|
|
}
|
|
assert.equal(documented.length, styles.length, "OpenAPI chart_style enum must list exactly the supported styles");
|
|
}
|