Files
RustProxy/scripts/tests/overview_samples_0330_ui_test.mjs
T
2026-10-16 13:02:53 +02:00

117 lines
6.4 KiB
JavaScript

import { renderAdminIndex } from './panel_html.mjs';
// Simulated browser/status contract: this does not exercise the Rust server.
import assert from 'node:assert/strict';
import fs from 'node:fs';
import path from 'node:path';
import vm from 'node:vm';
import { fileURLToPath } from 'node:url';
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..');
const read = file => fs.readFileSync(path.join(root, file), 'utf8');
const html = renderAdminIndex(root);
const controllerSource = read('src/admin/web/app/overview-samples.js');
const store = new Map();
assert.match(html, /id="overview-layout-unlock"/);
// History controls share the connected-core eyebrow row (not a separate
// dashboard row), and navigation shows them only on Overview.
const metaStart = html.indexOf('class="page-intro-meta-row"');
const metaEnd = html.indexOf('class="page-intro-title-row"');
const statusPos = html.indexOf('id="connection-label"');
const selectPos = html.indexOf('id="overview-live-samples"');
assert.ok(metaStart < statusPos && statusPos < selectPos && selectPos < metaEnd);
assert.ok(selectPos < html.indexOf('id="panel-overview"'));
assert.match(html, /id="overview-samples-toolbar" hidden role="group"/);
assert.match(read('src/admin/web/app/core.js'), /\$\("overview-samples-toolbar"\)\.hidden = name !== "overview"/);
assert.ok(html.indexOf('id="overview-live-samples"') < html.indexOf('id="overview-layout-unlock"'));
assert.ok(html.indexOf('id="overview-live-samples"') < html.indexOf('id="dashboard-overview-grid"'));
for (const sample of [60, 120, 300, 600]) assert.match(html, new RegExp(`<option value="${sample}">`));
class Element {
constructor() { this.listeners = {}; this.value = ''; this.dataset = {}; this.disabled = false; this.attributes = new Map(); this.textContent = ''; this.hidden = false; }
addEventListener(name, callback) { this.listeners[name] = callback; }
fire(name) { this.listeners[name](); }
setAttribute(name, value) { this.attributes.set(name, String(value)); }
getAttribute(name) { return this.attributes.get(name); }
}
const nodes = new Map();
for (const id of ['overview-live-samples', 'overview-samples-save', 'overview-samples-state']) nodes.set(id, new Element());
const widgets = new Map([['wide_compression_activity', new Element()]]);
const document = {
readyState: 'complete', getElementById: id => nodes.get(id) || null,
querySelector: query => widgets.get(query.match(/data-dashboard-widget="([^"]+)"/)?.[1]) || null,
};
const localStorage = {
getItem: key => store.get(key) ?? null,
setItem: (key, value) => { store.set(key, value); },
};
let now = 1000;
const window = { rustProxyRefresh: { current: () => ({ overview_seconds: 3 }) } };
const context = vm.createContext({ document, localStorage, window, queueMicrotask: callback => callback(),
t: key => key, performance: { now: () => now },
currentPanel: 'overview', latestStatus: null, $: id => { if (!nodes.has(id)) nodes.set(id, new Element()); return nodes.get(id); },
fmt: n => String(n), fmtBytes: n => `${n} B`,
});
vm.runInContext(controllerSource, context);
const samples = window.rustProxyOverviewSamples;
const select = document.getElementById('overview-live-samples');
const save = document.getElementById('overview-samples-save');
const state = document.getElementById('overview-samples-state');
assert.equal(samples.getLimit(), 60);
assert.equal(save.disabled, true);
select.value = '600'; select.fire('change');
assert.equal(samples.getLimit(), 600);
assert.equal(state.dataset.i18n, 'overviewSamplesTemporary');
assert.equal(store.size, 0, 'temporary samples must not touch localStorage');
assert.equal(save.disabled, false);
const range = Array.from({ length: 700 }, (_, i) => i);
let calls = 0;
samples.onChange(() => { samples.trim(range); calls++; });
select.value = '120'; select.fire('change');
assert.equal(calls, 1);
assert.equal(range.length, 120);
assert.equal(range[0], 580);
assert.equal(samples.setLimit(999), false);
assert.equal(samples.getLimit(), 120);
save.fire('click');
assert.equal(store.get('rustproxy.overview.liveChartSamples'), '120');
assert.equal(state.dataset.i18n, 'overviewSamplesSaved');
assert.equal(save.disabled, true);
// Fresh load reads only the saved choice, not a temporary override.
select.value = '300'; select.fire('change');
assert.equal(samples.getLimit(), 300);
const freshDocument = { readyState: 'loading', addEventListener() {} };
const fresh = vm.createContext({ document: freshDocument, localStorage, window: {}, t: key => key });
vm.runInContext(controllerSource, fresh);
assert.equal(fresh.window.rustProxyOverviewSamples.getLimit(), 120);
// Permanently saved choice survives a fresh load.
save.fire('click');
const again = vm.createContext({ document: freshDocument, localStorage, window: {}, t: key => key });
vm.runInContext(controllerSource, again);
assert.equal(again.window.rustProxyOverviewSamples.getLimit(), 300);
// Wide Overview charts must respect the same limit and redraw immediately when
// the limit shrinks, without recording an extra point or hitting the network.
vm.runInContext(read('src/admin/web/app/rate-sampling.js'), context);
vm.runInContext(read('src/admin/web/app/chart-scales.js'), context);
vm.runInContext(read('src/admin/web/app/overview-wide-metrics.js'), context);
select.value = '600'; select.fire('change');
const status = {
revision: 1, metrics: { compression_gzip: 0, compression_br: 0, compression_zstd: 0, compression_deflate: 0 },
};
context.latestStatus = status;
for (let i = 0; i <= 650; i++) {
now += 3000;
for (const key of ['compression_gzip', 'compression_br', 'compression_zstd', 'compression_deflate']) status.metrics[key] += i % 3 + 1;
window.rustProxyWideOverview.render(status);
}
const line = () => nodes.get('overview-wide-wide_compression_activity-gzip-line').getAttribute('d');
assert.equal((line().match(/ L/g) || []).length, 599, 'maximum 600 samples must be retained');
select.value = '120'; select.fire('change');
assert.equal((line().match(/ L/g) || []).length, 119, 'changing selection must trim and redraw without a new sample');
assert.equal(store.get('rustproxy.overview.liveChartSamples'), '300', 'temporary choice must preserve the saved limit');
assert.ok(!read('src/admin/web/app/overview-samples.js').includes('fetch('));
assert.ok(!read('src/admin/web/app/overview-samples.js').includes('setInterval('));
console.log('PASS: Overview samples 60/120/300/600, temporary vs persistent, immediate trim/redraw, no extra requests');