134 lines
7.0 KiB
JavaScript
134 lines
7.0 KiB
JavaScript
// Deterministic Overview widget regression; simulated DOM/status, no running API.
|
|
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 html = fs.readFileSync(path.join(root, 'src/admin/web/index.html'), 'utf8');
|
|
const registry = JSON.parse(fs.readFileSync(path.join(root, 'src/admin/web/dashboard-widgets.json'), 'utf8'));
|
|
const lang = JSON.parse(fs.readFileSync(path.join(root, 'langs/en_US.json'), 'utf8')).translations;
|
|
const code = file => fs.readFileSync(path.join(root, 'src/admin/web', file), 'utf8');
|
|
const widgets = new Map(), ids = new Map();
|
|
class Element {
|
|
constructor() { this.hidden = false; this.attributes = new Map(); this.style = {}; this.textContent = '-'; }
|
|
setAttribute(key, value) { this.attributes.set(key, String(value)); }
|
|
removeAttribute(key) { this.attributes.delete(key); }
|
|
getAttribute(key) { return this.attributes.get(key) ?? null; }
|
|
}
|
|
for (const [, id] of html.matchAll(/\bid="([^"]+)"/g)) {
|
|
assert.ok(!ids.has(id), `duplicate DOM id: ${id}`);
|
|
ids.set(id, new Element());
|
|
}
|
|
for (const [, id] of html.matchAll(/data-dashboard-widget="([^"]+)"/g)) widgets.set(id, new Element());
|
|
assert.equal(registry.length, widgets.size, 'registry and DOM widget count must match');
|
|
for (const entry of registry) assert.ok(widgets.has(entry.id), `registry references missing DOM card ${entry.id}`);
|
|
let now = 1000;
|
|
const document = {
|
|
hidden: false,
|
|
addEventListener() {},
|
|
querySelector(query) { return widgets.get(query.match(/^\[data-dashboard-widget="([^"]+)"\]$/)?.[1]) ?? null; },
|
|
};
|
|
const window = { addEventListener() {}, rustProxyRefresh: { current: () => ({ overview_seconds: 3 }) } };
|
|
const context = vm.createContext({
|
|
window, document, performance: { now: () => now },
|
|
currentPanel: 'overview', $: id => ids.get(id) ?? null,
|
|
fmt: (v, digits = 0) => Number(v).toFixed(digits),
|
|
fmtBytes: v => `${Number(v)} B`,
|
|
t: key => lang[key] || key,
|
|
console,
|
|
});
|
|
for (const script of ['app/rate-sampling.js', 'app/overview-extra-metrics.js', 'app/overview-wide-metrics.js']) {
|
|
vm.runInContext(code(script), context, { filename: script });
|
|
}
|
|
const extra = window.rustProxyExtraOverview, wide = window.rustProxyWideOverview;
|
|
const status = {
|
|
revision: 9,
|
|
system: { open_fds: 60, max_fds: 300 },
|
|
cache: { hits: 100, misses: 50, resident_bytes: 150, hot_limit_bytes: 300,
|
|
cold_queue_bytes: 25, cold_queue_limit_bytes: 100, cold_bytes: 400, cold_limit_bytes: 1000 },
|
|
metrics: { requests: 300, http1_requests: 200, http2_requests: 80, http3_requests: 20,
|
|
compression_gzip: 10, compression_br: 10, compression_zstd: 10, compression_deflate: 1 },
|
|
transform_metrics: { applied: 10, cache_hits: 10, skipped: 10, errors: 1 },
|
|
};
|
|
extra.render(status);
|
|
wide.render(status);
|
|
assert.equal(ids.get('overview-extra-extra_cache_memory-meter-fill').style.width, '50%');
|
|
assert.equal(ids.get('overview-extra-extra_cold_queue_memory-meter-fill').style.width, '25%');
|
|
assert.equal(ids.get('overview-extra-extra_cold_storage-meter-fill').style.width, '40%');
|
|
widgets.get('extra_cache_memory').hidden = false;
|
|
widgets.get('extra_cache_hit_rate').hidden = false;
|
|
widgets.get('extra_h2_share').hidden = false;
|
|
widgets.get('extra_h1_share').hidden = false;
|
|
widgets.get('extra_h3_share').hidden = false;
|
|
widgets.get('extra_process_fds').hidden = false;
|
|
extra.render(status);
|
|
assert.equal(ids.get('overview-extra-extra_process_fds-meter-fill').style.width, '20%');
|
|
assert.equal(ids.get('overview-extra-extra_cache_hit_rate').textContent, '-'); // first counter sample has no rate
|
|
assert.equal(ids.get('overview-wide-wide_transform_activity-summary').textContent, '-');
|
|
|
|
now = 4000;
|
|
status.cache.hits += 30;
|
|
status.cache.misses += 10;
|
|
status.metrics.requests += 40;
|
|
status.metrics.http1_requests += 10;
|
|
status.metrics.http2_requests += 20;
|
|
status.metrics.http3_requests += 10;
|
|
status.metrics.compression_gzip += 3;
|
|
status.metrics.compression_br += 6;
|
|
status.metrics.compression_zstd += 9;
|
|
status.metrics.compression_deflate += 12;
|
|
status.transform_metrics.applied += 15;
|
|
status.transform_metrics.cache_hits += 3;
|
|
status.transform_metrics.skipped += 6;
|
|
status.transform_metrics.errors += 1;
|
|
extra.render(status);
|
|
wide.render(status);
|
|
assert.equal(ids.get('overview-extra-extra_cache_hit_rate').textContent, '75.0%');
|
|
assert.equal(ids.get('extra-cache-ratio-hit-arc').getAttribute('stroke-dasharray'), '75 25');
|
|
assert.equal(ids.get('extra-cache-ratio-miss-arc').getAttribute('stroke-dashoffset'), '-75');
|
|
assert.equal(ids.get('extra-cache-ratio-hit-count').textContent, '30');
|
|
assert.equal(ids.get('extra-cache-ratio-miss-count').textContent, '10');
|
|
assert.equal(ids.get('overview-extra-extra_h2_share-meter-fill').style.width, '50%');
|
|
assert.equal(ids.get('overview-wide-wide_transform_activity-summary').textContent, '8.3/s');
|
|
assert.equal(ids.get('overview-wide-wide_compression_activity-summary').textContent, '10.0/s');
|
|
for (const name of ['applied', 'cache_hits', 'skipped', 'errors'])
|
|
assert.ok(ids.get(`overview-wide-wide_transform_activity-${name}-line`).getAttribute('d')?.length);
|
|
for (const name of ['gzip', 'br', 'zstd', 'deflate'])
|
|
assert.ok(ids.get(`overview-wide-wide_compression_activity-${name}-line`).getAttribute('d')?.length);
|
|
|
|
// No hidden-tab spike or fake zero when a long interval is skipped.
|
|
now = 100000;
|
|
status.cache.hits += 300;
|
|
status.cache.misses += 200;
|
|
status.metrics.compression_gzip += 600;
|
|
extra.render(status);
|
|
wide.render(status);
|
|
assert.equal(ids.get('overview-extra-extra_cache_hit_rate').textContent, '-');
|
|
assert.equal(ids.get('overview-wide-wide_compression_activity-summary').textContent, '-');
|
|
now = 103000;
|
|
status.cache.hits += 5;
|
|
status.cache.misses += 5;
|
|
status.metrics.compression_gzip += 3;
|
|
extra.render(status);
|
|
wide.render(status);
|
|
assert.equal(ids.get('overview-extra-extra_cache_hit_rate').textContent, '50.0%');
|
|
assert.equal(ids.get('overview-wide-wide_compression_activity-summary').textContent, '1.0/s');
|
|
|
|
// Bounded metric with no configured budget remains unknown, not a made-up percentage.
|
|
status.cache.cold_limit_bytes = 0;
|
|
extra.render(status);
|
|
assert.equal(ids.get('overview-extra-extra_cold_storage').textContent, '400 B');
|
|
assert.equal(ids.get('overview-extra-extra_cold_storage-meter').getAttribute('aria-valuenow'), null);
|
|
assert.equal(ids.get('overview-extra-extra_cold_storage-meter-fill').style.width, '0%');
|
|
|
|
extra.donut('cache-ratio', 75, 25);
|
|
assert.equal(ids.get('cache-ratio-hit-arc').getAttribute('stroke-dasharray'), '75 25');
|
|
assert.equal(ids.get('cache-ratio-chart').getAttribute('aria-label'), 'HIT 75, MISS 25');
|
|
extra.donut('cache-ratio', 0, 0);
|
|
assert.equal(ids.get('cache-ratio-hit-arc').getAttribute('stroke-dasharray'), '0 100');
|
|
assert.ok(code('app/runtime.js').includes('rustProxyExtraOverview?.donut?.("cache-ratio"'),
|
|
'main HIT/MISS tile must share tested donut rendering');
|
|
console.log('PASS: bounded meters, COLD tiers, HIT/MISS rings, transform and compression rates, pause/no-limit');
|