// 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');