This commit is contained in:
Mateusz Gruszczyński committed 2026-10-13 00:00:06 +02:00
1 parent d23fa5e154
commit 81ab39e70a
51 files changed
+5352 -1489

No files matched your search

@@ -0,0 +1,120 @@
// Regression for persisted Appearance preferences and Stream widgets.
// Uses the actual JS functions with a minimal DOM, no network or timers.
import assert from 'node:assert/strict';
import fs from 'node:fs';
import vm from 'node:vm';
const prefsSource = fs.readFileSync('src/admin/web/app/preferences.js', 'utf8');
const streamSource = fs.readFileSync('src/admin/web/app/stream-dashboard.js', 'utf8');
const html = fs.readFileSync('src/admin/web/index.html', 'utf8');
const css = fs.readFileSync('src/admin/web/style.css', 'utf8');
const ids = ['stream_overview', 'stream_traffic', 'stream_sessions', 'stream_backend_health'];
assert(html.includes('id="panel-overview"'));
for (const id of ids) assert(html.includes(`data-dashboard-widget="${id}"`));
assert.doesNotMatch(css, /#panel-overview \.stats-grid \.stat:is\(:hover, :focus-visible, :focus-within\)/,
'Overview details must never become hover/focus popovers');
assert.match(css, /#panel-overview \.stats-grid \.stat-chart > \.stat-chart-legend/,
'in-card chart legends must be visible, including on touch devices');
assert.match(css, /#panel-overview \.stats-grid \{ grid-template-columns: repeat\(6, minmax\(0, 1fr\)\); \}/,
'desktop Overview must retain the original six cards per row');
assert.doesNotMatch(css, /#panel-overview \.stats-grid \{ grid-template-columns: repeat\(4, minmax\(0, 1fr\)\); \}/,
'do not replace the original six-column Overview with a four-column layout');
assert.match(css, /#panel-overview \.stats-grid > \.stat \{ min-height: 126px;/,
'cards should be tall enough for their inline metric legends');
assert.match(html, /id="stream-dashboard-traffic-detail"/);
assert.match(html, /id="stream-dashboard-rx-rate"/);
assert.match(html, /id="stream-dashboard-tx-rate"/);
assert.doesNotMatch(html.slice(html.indexOf('id="panel-overview"'), html.indexOf('class="overview-grid"')), /<article[^>]*tabindex="0"/,
'metric cards must not introduce redundant keyboard stops');
const cardIds = [...html.matchAll(/<article class="stat[^"]*" data-dashboard-widget="([^"]+)"/g)].map(m => m[1]);
assert.equal(cardIds.length, 16, 'existing Overview cards must be retained');
const cards = new Map(cardIds.map(id => [id, { dataset: { dashboardWidget: id }, hidden: id.startsWith('stream_') }]));
const grid = { querySelectorAll: () => [...cards.values()], append(node) { assert(cards.has(node.dataset.dashboardWidget)); } };
const elements = new Map();
for (const name of ['overview', 'overview-detail', 'traffic', 'traffic-detail', 'sessions', 'sessions-detail', 'health', 'health-detail', 'rx', 'tx', 'rx-rate', 'tx-rate']) {
elements.set('stream-dashboard-' + name, { textContent: '-', attrs: {}, setAttribute(k, v) { this.attrs[k] = v; } });
}
const status = { streams: {
active: [{ id: 'alpha', protocol: 'tcp', enabled: true }],
desired: [{ id: 'alpha', protocol: 'tcp', enabled: true }],
metrics: { alpha: { active: 5, bytes_in: 1024, bytes_out: 2048, backends: [{ id: 'a', enabled: true, status: 'healthy' }] } }
} };
const window = { rustProxyRefresh: { update() {} } };
const context = {
window, latestStatus: status, envelope: { config: { streams: status.streams.desired } },
currentPanel: 'overview', authenticated: true, languagePacks: { size: 0 },
$: id => elements.get(id), document: { querySelector(selector) {
if (selector === '#panel-overview .stats-grid') return grid;
const match = selector.match(/^\[data-dashboard-widget="([^"]+)"\]$/);
return match ? cards.get(match[1]) : null;
} }, performance: { now: () => 1000 }, t: key => key,
fmt: v => String(v), bytes: v => v + ' B', Math,
normalizeRefreshPreferences: v => v || {}, renderRefreshPreferences() {},
applyAdminTheme: v => v, applyAdminLanguage: v => v, applyAdminUiScale: v => v,
applyAdminTimeFormat: v => v, applyAdminChartStyle: v => v,
applyAdminButtonStyle: v => v, applyAdminTableDensity: v => v,
applyAdminDashboardDensity: v => v, refreshAppearanceUi() {},
};
vm.createContext(context);
const constants = prefsSource.slice(prefsSource.indexOf('const ADMIN_DASHBOARD_WIDGETS'), prefsSource.indexOf('const CHART_STYLE_KEYS'));
const normalize = prefsSource.slice(prefsSource.indexOf('function normalizeDashboardWidgets'), prefsSource.indexOf('function browserLanguage'));
const applyLayout = prefsSource.slice(prefsSource.indexOf('function applyDashboardLayout'), prefsSource.indexOf('function dashboardWidgetLabel'));
const applyPreferences = prefsSource.slice(prefsSource.indexOf('function applyAdminPreferences'), prefsSource.indexOf('try { applyAdminTheme('));
vm.runInContext(`${constants}\n${normalize}\nlet adminPreferences = null;\n${applyLayout}\n${applyPreferences}\nthis.getPrefs = () => adminPreferences;\nthis.applyAdminPreferencesTest = applyAdminPreferences;`, context);
window.rustProxyPreferences = { current: () => context.getPrefs() };
vm.runInContext(streamSource, context);
// First load (or page reload): defaults hide the new widgets.
const initial = { dashboard_widgets: cardIds, dashboard_hidden_widgets: ids };
context.applyAdminPreferencesTest(initial);
assert(ids.every(id => cards.get(id).hidden), 'opt-in cards must start hidden');
// Server responds to a previously saved opt-in; at this point the old in-memory
// prefs are still hidden. The new layout must win before adminPreferences is set.
context.applyAdminPreferencesTest({ dashboard_widgets: cardIds, dashboard_hidden_widgets: [] });
assert(ids.every(id => !cards.get(id).hidden), 'server-saved opt-in must reveal cards immediately');
assert.equal(elements.get('stream-dashboard-sessions').textContent, '5', 'no need to wait for polling');
assert.equal(elements.get('stream-dashboard-overview').textContent, '1 / 1');
assert.equal(elements.get('stream-dashboard-traffic').textContent, '-', 'cached repaint is not a traffic sample');
// Hide one widget and restore it, all within the same session. Normal cards stay put.
context.applyAdminPreferencesTest({ dashboard_widgets: cardIds, dashboard_hidden_widgets: ['stream_traffic'] });
assert(cards.get('stream_traffic').hidden);
assert(!cards.get('requests').hidden);
context.applyAdminPreferencesTest({ dashboard_widgets: cardIds, dashboard_hidden_widgets: [] });
assert(!cards.get('stream_traffic').hidden);
// A disabled/absent Stream must not override an explicit Appearance opt-in.
context.envelope.config.streams = [];
context.latestStatus = { streams: { active: [], desired: [], metrics: {} } };
context.applyAdminPreferencesTest({ dashboard_widgets: cardIds, dashboard_hidden_widgets: [] });
assert(ids.every(id => !cards.get(id).hidden), 'show requested cards even with no configured Stream');
assert.equal(elements.get('stream-dashboard-overview').textContent, '-');
assert.equal(elements.get('stream-dashboard-overview-detail').textContent, 'streamWidgetUnavailable');
context.applyAdminPreferencesTest({ dashboard_widgets: cardIds, dashboard_hidden_widgets: ids });
assert(ids.every(id => cards.get(id).hidden), 'hiding cards must still work');
assert(!cards.get('requests').hidden, 'legacy widgets must retain their visibility');
assert.match(css, /dashboard-widget-setting:is\(:hover, :focus-within\) \.dashboard-widget-description/, 'description is a tooltip');
assert.match(css, /\.dashboard-widget-description \{[^}]*position: absolute/s, 'tooltip must not occupy a second row');
assert.match(prefsSource, /toggle\.setAttribute\("aria-describedby", description\.id\)/, 'description must be accessible');
// Editor-specific regressions: avoid the redundant enable hint and label the
// two distinct inheritance layers instead of saying "Inherit" everywhere.
const editor = fs.readFileSync('src/admin/web/streams.js', 'utf8');
const pl = JSON.parse(fs.readFileSync('langs/pl_pl.json', 'utf8')).translations;
const en = JSON.parse(fs.readFileSync('langs/en_US.json', 'utf8')).translations;
assert(editor.includes('enabledLabel?.querySelector(".vh-enable-hint")?.remove()'));
const vhostEditor = fs.readFileSync('src/admin/web/vhosts/editor.js', 'utf8');
assert.match(vhostEditor, /window\.rustProxyEnableControl\(enabledControl, "vhEnableHint"\)/);
assert.match(vhostEditor, /enabledLabel\?\.querySelector\("\.vh-enable-hint"\)\?\.remove\(\)/, 'vhost editor must not show the redundant active hint');
assert.match(vhostEditor, /enabledLabel\?\.classList\.add\("vh-enable-no-hint"\)/);
assert(editor.includes('["inherit", "streamMetricsDefaultOff"]'));
assert(editor.includes('["inherit", "streamMetricsFromStream"]'));
for (const locale of [pl, en]) {
for (const key of ['streamMetricsDefaultOff', 'streamMetricsFromStream', 'streamMetricsHelp', 'streamBackendDetailMetrics'])
assert(locale[key] && locale[key].length > 5, `missing ${key}`);
}
assert.equal(pl.streamMetricsFromStream, 'Z ustawień Stream');
assert.equal(pl.streamMetricsDefaultOff, 'Domyślnie wyłączone');
console.log('PASS: Stream visibility, inline Overview legends, preserved Appearance tooltip and Vhost enable label.');