104 lines
6.5 KiB
JavaScript
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.');
|