115 lines
6.3 KiB
JavaScript
115 lines
6.3 KiB
JavaScript
// 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 = read('src/admin/web/index.html');
|
|
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/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');
|