140 lines
8.2 KiB
JavaScript
140 lines
8.2 KiB
JavaScript
// Run with: node tests/docker_vhost_metrics.cjs
|
|
// Source contracts and isolated DOM/metric sampling regression for Docker vhosts.
|
|
'use strict';
|
|
const assert = require('node:assert/strict');
|
|
const fs = require('node:fs');
|
|
const path = require('node:path');
|
|
const vm = require('node:vm');
|
|
const root = path.resolve(__dirname, '..');
|
|
const source = name => fs.readFileSync(path.join(root, name), 'utf8');
|
|
const docker = source('src/admin/web/docker.js');
|
|
const core = source('src/admin/web/app/core.js');
|
|
const html = source('src/admin/web/index.html');
|
|
const css = source('src/admin/web/style.css');
|
|
const en = JSON.parse(source('langs/en_US.json')).translations;
|
|
const pl = JSON.parse(source('langs/pl_pl.json')).translations;
|
|
|
|
assert.match(html, /id="docker-metrics-toggle-all"/);
|
|
assert.match(docker, /if \(agent\.enabled !== false && assignment && assignment\.enabled !== false\) row\.append\(statisticsSection\(agent, key\)\)/);
|
|
assert.match(docker, /"docker-service-editor docker-service-metrics"/);
|
|
assert.ok(docker.indexOf('row.append(editor);') < docker.indexOf('row.append(statisticsSection(agent, key))'));
|
|
for (const key of ['status', 'vhRequests', 'vhActive', 'vhErrors', 'vhLatency', 'vhTraffic', 'vhLifetimeChart']) {
|
|
assert.ok(docker.includes(`"${key}"`), `missing ordinary Vhost metric: ${key}`);
|
|
}
|
|
for (const key of ['dockerTrafficStatistics', 'dockerExpandAllMetrics', 'dockerCollapseAllMetrics']) {
|
|
assert.ok(en[key] && pl[key], `missing translated label: ${key}`);
|
|
}
|
|
for (const className of ['vhost-live-grid', 'vhost-live-metric', 'vhost-chart', 'vhost-chart-line']) {
|
|
assert.ok(css.includes(`.${className}`), `existing ordinary Vhost style missing: ${className}`);
|
|
}
|
|
assert.match(docker, /status\?\.vhost_metrics/);
|
|
assert.match(docker, /rustProxyRateSampling\.difference/);
|
|
assert.match(docker, /docker-service-editor:not\(\.docker-service-metrics\)\[open\]/);
|
|
assert.match(docker, /expandedMetrics\.clear\(\); metricHistory\.clear\(\)/);
|
|
assert.match(core, /window\.rustProxyDocker\?\.renderStatus\(s\)/);
|
|
|
|
// Keep the production module intact. Expose only local functions for this
|
|
// self-contained DOM stub; no browser, network, or Rust server is involved.
|
|
class Node {
|
|
constructor(tag) { this.tag = tag; this.children = []; this.dataset = {}; this.className = ''; this.listeners = {}; this.attributes = {}; this.open = false; this.disabled = false; this._text = ''; }
|
|
append(...children) { this.children.push(...children); }
|
|
replaceChildren(...children) { this.children = [...children]; this._text = ''; }
|
|
get textContent() { return this._text + this.children.map(node => node.textContent).join(''); }
|
|
set textContent(value) { this.replaceChildren(); this._text = String(value); }
|
|
setAttribute(name, value) { this.attributes[name] = String(value); }
|
|
getAttribute(name) { return this.attributes[name]; }
|
|
addEventListener(name, callback) { (this.listeners[name] ||= []).push(callback); }
|
|
dispatch(name) { for (const callback of this.listeners[name] || []) callback({target:this}); }
|
|
click() { this.dispatch('click'); }
|
|
}
|
|
const controls = new Map([['docker-metrics-toggle-all', new Node('button')], ['docker-discovery-list', new Node('div')]]);
|
|
let sections = [], now = 1000;
|
|
const document = {
|
|
hidden: false,
|
|
createElementNS(ns, tag) { return new Node(tag); },
|
|
querySelectorAll(selector) { if (selector === '#docker-discovery-list .docker-agent-card') return []; assert.equal(selector, '#docker-discovery-list .docker-service-metrics'); return sections; },
|
|
addEventListener() { },
|
|
};
|
|
const context = {
|
|
window: {addEventListener(){}, rustProxyPreferences:{}, rustProxyAclPicker:{}},
|
|
document, performance: {now:() => now},
|
|
localStorage:{getItem(){return null;}},
|
|
$: id => controls.get(id) || null,
|
|
el(tag, value, classes) { const node = new Node(tag); if (value !== undefined) node.textContent = value; if (classes) node.className = classes; return node; },
|
|
t: key => en[key] || key,
|
|
fmt: value => Number(value).toLocaleString('en-US', {maximumFractionDigits:1}),
|
|
rateBytes: value => `${(Number(value)/1024).toFixed(1)} KiB`,
|
|
locale: () => 'en-US',
|
|
rustProxyRefresh: {register(){}, current(){return {overview_seconds:3};}},
|
|
latestStatus:null, authenticated:false, currentPanel:'overview',
|
|
};
|
|
vm.createContext(context);
|
|
vm.runInContext(source('src/admin/web/app/rate-sampling.js'), context);
|
|
const marker = '})();';
|
|
assert.ok(docker.trimEnd().endsWith(marker));
|
|
const injected = docker.trimEnd().slice(0, -marker.length) + `
|
|
window._metricsTest = {
|
|
statisticsSection, renderDockerMetrics, syncMetricsToggle,
|
|
setRuntime(routes, pools) { poolRoutes = routes; poolIndex = new Map(pools.map(pool => [pool.id, pool])); },
|
|
};
|
|
})();`;
|
|
vm.runInContext(injected, context);
|
|
const metrics = context.window._metricsTest;
|
|
const agent = {id:'docker-1'};
|
|
const first = metrics.statisticsSection(agent, 'service/a');
|
|
const second = metrics.statisticsSection(agent, 'service-b');
|
|
sections = [first, second];
|
|
const button = controls.get('docker-metrics-toggle-all');
|
|
metrics.syncMetricsToggle();
|
|
assert.ok(sections.every(section => !section.open), 'default collapsed');
|
|
assert.equal(button.textContent, en.dockerExpandAllMetrics);
|
|
assert.equal(button.disabled, false);
|
|
assert.equal(first.className, 'docker-service-editor docker-service-metrics');
|
|
assert.equal(first.children[0].tag, 'summary');
|
|
assert.equal(first.children[0].children[0].textContent, en.dockerTrafficStatistics);
|
|
assert.equal(first.children[1].className, 'vhost-live-grid');
|
|
assert.equal(first.children[1].children.length, 6);
|
|
button.click();
|
|
assert.ok(sections.every(section => section.open), 'one click expands every host');
|
|
assert.equal(button.textContent, en.dockerCollapseAllMetrics);
|
|
const refreshed = metrics.statisticsSection(agent, 'service/a');
|
|
assert.equal(refreshed.open, true, 'expanded state survives re-render');
|
|
button.click();
|
|
assert.ok(sections.every(section => !section.open), 'second click collapses all');
|
|
first.open = true; first.dispatch('toggle');
|
|
assert.equal(button.textContent, en.dockerExpandAllMetrics, 'partial expansion still offers expand all');
|
|
|
|
metrics.setRuntime([
|
|
{agent_id:'docker-1',service_key:'service/a',pool_id:'docker-generated-a'},
|
|
{agent_id:'docker-1',service_key:'service-b',pool_id:'docker-generated-b'},
|
|
], [
|
|
{id:'docker-generated-a',health_mode:'http',members:[{status:'up'}]},
|
|
{id:'docker-generated-b',health_mode:'tcp',members:[{status:'down'}]},
|
|
]);
|
|
const base = {requests:26288, active:7,errors:129,ewma_latency_ms:20.4,bytes_in:15000,bytes_out:2000};
|
|
metrics.renderDockerMetrics({vhost_metrics:{'docker-generated-a':base,'docker-generated-b':{...base,requests:22}}});
|
|
assert.equal(first.dockerMetricView.fields.status.textContent, '1/1 Up');
|
|
assert.equal(first.dockerMetricView.fields.vhRequests.textContent, '26,288');
|
|
assert.equal(first.dockerMetricView.fields.vhActive.textContent, '7');
|
|
assert.equal(first.dockerMetricView.fields.vhErrors.textContent, '129');
|
|
assert.equal(first.dockerMetricView.fields.vhLatency.textContent, '20.4 ms');
|
|
assert.match(first.dockerMetricView.chartValue.textContent, /req\/s .* peak/);
|
|
now += 3000;
|
|
const increased = {...base, requests:26300, bytes_out:32720};
|
|
metrics.renderDockerMetrics({vhost_metrics:{'docker-generated-a':increased,'docker-generated-b':{...base,requests:22}}, backend_pools:[
|
|
{id:'docker-generated-a',health_mode:'http',members:[{status:'down'}]}
|
|
]});
|
|
assert.equal(first.dockerMetricView.fields.vhTraffic.textContent, '10.0 KiB/s');
|
|
assert.equal(first.dockerMetricView.fields.status.textContent, '0/1 Up', 'live status overrides the less frequent Docker inventory snapshot');
|
|
assert.match(first.dockerMetricView.chartValue.textContent, /^4\.0 req\/s/);
|
|
assert.ok(first.dockerMetricView.line.getAttribute('points').length > 5, 'chart receives sampled points');
|
|
// Repaint of an unchanged status snapshot must not create a false 0 req/s sample.
|
|
now += 500;
|
|
metrics.renderDockerMetrics({vhost_metrics:{'docker-generated-a':increased}});
|
|
assert.match(first.dockerMetricView.chartValue.textContent, /^4\.0 req\/s/);
|
|
sections = [];
|
|
context.window.rustProxyDocker.reset();
|
|
assert.equal(button.disabled, true, 'no active hosts disables expand-all');
|
|
assert.equal(metrics.statisticsSection(agent, 'service/a').open, false, 'sign-out resets expansion');
|
|
console.log('Docker vhost statistics, shared classes, all/individual toggle and live counters: PASS');
|