Files
RustProxy/tools/checks/stream_0320_ui_test.mjs
T
2026-10-13 21:29:57 +02:00

104 lines
6.5 KiB
JavaScript

// Verify the real opt-in Stream Dashboard component against a lightweight DOM.
// No server, browser layout engine or Rust executable is exercised here.
import assert from 'node:assert/strict';
import fs from 'node:fs';
import vm from 'node:vm';
const src = fs.readFileSync('src/admin/web/app/stream-dashboard.js', 'utf8');
const preferencesSource = fs.readFileSync('src/admin/web/app/preferences.js', 'utf8');
const ids = ['stream_overview', 'stream_traffic', 'stream_sessions', 'stream_backend_health'];
assert(!/\b(setInterval|setTimeout|fetch|XMLHttpRequest)\s*\(/.test(src), 'widgets must not create timers/fetches');
const cards = new Map(ids.map(id => [id, { hidden: true }]));
const elements = new Map();
for (const name of ['overview', 'overview-detail', 'sessions', 'sessions-detail', 'health', 'health-detail', 'traffic', 'traffic-detail', 'rx', 'tx', 'rx-rate', 'tx-rate']) {
elements.set(`stream-dashboard-${name}`, { textContent: '-', attrs: new Map(), setAttribute(k, v) { this.attrs.set(k, v); } });
}
const preferences = { dashboard_hidden_widgets: [...ids] };
const window = { rustProxyPreferences: { current: () => preferences } };
let clock = 1000;
const scriptContext = { window, document: { querySelector(selector) {
const match = selector.match(/^\[data-dashboard-widget="([^\"]+)"\]$/);
return match ? cards.get(match[1]) || null : null;
} }, $: name => elements.get(name), ADMIN_APPEARANCE_DEFAULTS: { dashboard_hidden_widgets: ids },
envelope: { config: { streams: [
{ id: 'tcp', enabled: true, protocol: 'tcp' },
{ id: 'udp', enabled: true, protocol: 'udp' }
] } }, latestStatus: null, currentPanel: 'overview',
performance: { now: () => clock }, t: key => key, fmt: n => `${n}`, bytes: n => `${n} B`, Math,
};
vm.runInNewContext(src, scriptContext);
const widget = window.rustProxyStreamDashboard;
const status = { streams: { active: [
{ id: 'tcp', enabled: true, protocol: 'tcp' },
{ id: 'udp', enabled: true, protocol: 'udp' }
], metrics: {
tcp: { active: 3, bytes_in: 500, bytes_out: 1000, backends: [
{ id: 'a', enabled: true, drain: false, status: 'healthy' },
{ id: 'b', enabled: true, drain: false, status: 'unhealthy' }
] },
udp: { active: 2, bytes_in: 100, bytes_out: 200, backends: [
{ id: 'a', enabled: true, drain: false, status: 'unverified' }
] }
} } };
scriptContext.latestStatus = status;
widget.render(status);
assert(ids.every(id => cards.get(id).hidden), 'new widgets are hidden by default');
assert.equal(elements.get('stream-dashboard-traffic').textContent, '-', 'hidden chart must not sample');
preferences.dashboard_hidden_widgets = [];
widget.render(status);
assert(ids.every(id => !cards.get(id).hidden));
assert.equal(elements.get('stream-dashboard-sessions').textContent, '5');
assert.equal(elements.get('stream-dashboard-overview').textContent, '2 / 2');
assert.equal(elements.get('stream-dashboard-health').textContent, '1 / 1');
clock += 1000;
status.streams.metrics.tcp.bytes_in += 1024;
status.streams.metrics.tcp.bytes_out += 512;
widget.render(status);
assert.equal(elements.get('stream-dashboard-rx-rate').textContent, '1024 B/s');
assert.equal(elements.get('stream-dashboard-tx-rate').textContent, '512 B/s');
assert(elements.get('stream-dashboard-rx').attrs.get('points')?.includes(','));
preferences.dashboard_hidden_widgets = ['stream_traffic'];
widget.syncVisibility();
assert(cards.get('stream_traffic').hidden);
const previous = elements.get('stream-dashboard-rx').attrs.get('points');
clock += 1000;
widget.render(status);
assert.equal(elements.get('stream-dashboard-rx').attrs.get('points'), previous, 'hidden chart must not update');
// Explicitly enabled cards remain visible even without active/configured streams.
// The user should see a neutral state, not a widget that ignores Appearance.
preferences.dashboard_hidden_widgets = [];
scriptContext.envelope.config.streams = [];
scriptContext.latestStatus = { streams: { active: [], metrics: {} } };
widget.render(scriptContext.latestStatus);
assert(ids.every(id => !cards.get(id).hidden), 'appearance must control Stream card visibility even with no listeners');
for (const name of ['overview', 'sessions', 'health', 'traffic']) {
assert.equal(elements.get(`stream-dashboard-${name}`).textContent, '-');
if (name !== 'traffic') assert.equal(elements.get(`stream-dashboard-${name}-detail`).textContent, 'streamWidgetUnavailable');
}
assert.equal(elements.get('stream-dashboard-rx-rate').textContent, '-');
assert.equal(elements.get('stream-dashboard-tx-rate').textContent, '-');
assert.equal(elements.get('stream-dashboard-rx').attrs.get('points'), '0,55 300,55', 'empty status must clear the graph');
// A desired but not yet running Stream must still make the overview useful.
scriptContext.envelope.config.streams = [{ id: 'pending', enabled: true, protocol: 'tcp' }];
preferences.dashboard_hidden_widgets = [];
widget.render({ streams: { active: [], metrics: {} } });
assert.equal(elements.get('stream-dashboard-overview').textContent, '0 / 1');
assert(!cards.get('stream_overview').hidden, 'restart-pending listener does not suppress the card');
preferences.dashboard_hidden_widgets = [...ids];
widget.render(scriptContext.latestStatus);
assert(ids.every(id => cards.get(id).hidden), 'only the Appearance switch hides opt-in cards');
widget.reset();
// Reuse original Appearance normalization logic to protect 0.31.x saved layouts.
const registryPrelude = `const ADMIN_DASHBOARD_WIDGET_REGISTRY = Object.freeze(${fs.readFileSync("src/admin/web/dashboard-widgets.json", "utf8")});`;
const definitions = preferencesSource.slice(preferencesSource.indexOf('const ADMIN_DASHBOARD_WIDGETS'), preferencesSource.indexOf('const CHART_STYLE_KEYS'));
const normalize = preferencesSource.slice(preferencesSource.indexOf('function normalizeDashboardWidgets'), preferencesSource.indexOf('function browserLanguage'));
const sandbox = {};
vm.runInNewContext(`${registryPrelude}\n${definitions}\n${normalize}\nthis.normalizeDashboardWidgets = normalizeDashboardWidgets; this.normalizeHiddenDashboardWidgets = normalizeHiddenDashboardWidgets`, sandbox);
const oldOrder = ['requests', 'errors'];
const migratedOrder = [...sandbox.normalizeDashboardWidgets(oldOrder)];
const migratedHidden = [...sandbox.normalizeHiddenDashboardWidgets([], oldOrder)];
assert.deepEqual(migratedOrder.slice(0, 2), oldOrder);
assert(ids.every(id => migratedOrder.includes(id) && migratedHidden.includes(id)), 'legacy order must add cards as hidden');
console.log('PASS: opt-in Stream dashboard uses shared data with no polling, handles user visibility/empty-status states, and migrates old Appearance layouts.');