Files
RustProxy/tools/checks/live_diagnostics_0326_browser.py
T
2026-10-14 11:51:37 +02:00

134 lines
8.2 KiB
Python
Executable File

#!/usr/bin/env python3
"""Real diagnostic renderer/CSS in Chromium mobile emulation (not iOS Safari)."""
from pathlib import Path
import shutil
from bs4 import BeautifulSoup
from playwright.sync_api import sync_playwright
ROOT = Path(__file__).resolve().parents[2]
WEB = ROOT / 'src/admin/web'
source = (WEB / 'app/runtime.js').read_text()
soup = BeautifulSoup((WEB / 'index.html').read_text(), 'html.parser')
process = soup.select_one('[data-dashboard-widget="process_diagnostics"]')
network = soup.select_one('[data-dashboard-widget="network_diagnostics"]')
assert process and network
# Run the real renderDiagnostics and interface component as shipped, without
# creating a fake SSE server or depending on Rust/Cargo in this test environment.
start = source.index('let systemSamples =')
end = source.index('function renderHeaderRules(', start)
renderer = source[start:end]
html = ('<!doctype html><html data-theme="ultra_dark" data-corner-style="rounded" '
'data-dashboard-density="compact"><head><meta name="viewport" '
'content="width=device-width, initial-scale=1"><style>'
+ (WEB / 'style.css').read_text() + '</style></head>'
'<body style="margin:0"><div style="height:1100px">Earlier dashboard cards</div>'
'<section id="panel-overview"><div id="dashboard-overview-grid" '
'class="stats-grid dashboard-overview-grid is-aligned">'
+ str(process) + str(network) + '</div></section>'
'<div class="vhost-policy-pills"><small class="pill">H1</small>'
'<small class="vhost-backend-protocol-badge" data-observed="true">AUTO → H2</small></div>'
'<div style="height:900px">Footer</div></body></html>')
stubs = '''
const dummy = new Map();
const $ = id => document.getElementById(id) || (() => {
if (!dummy.has(id)) dummy.set(id, { textContent:'-', value:'', hidden:false,
classList:{ toggle(){} }, setAttribute(){}, removeAttribute(){}, append(){},
replaceChildren(){}, closest(){ return {hidden:false}; } });
return dummy.get(id);
})();
const envelope = {config:{cache:{enabled:true,cold_enabled:false}}};
const t = key => ({networkInterfacesVisibleCount:'{visible} / {total}',
networkInterfaceRate:'Rate',networkInterfacesHistory:'Traffic',hideMetric:'Hide',
showMetric:'Show',hiddenInterfaces:'Hidden',networkNoInterfaces:'None',
networkNoVisibleInterfaces:'None visible',notEnabled:'Disabled',
activeListeners:'Listeners',restartPending:'Restart',realSample:'Sampled',
threads:'threads',unavailable:'Unavailable',unlimited:'Unlimited'})[key] || key;
const bytes = value => `${Number(value)} B`;
const locale = () => 'en-US';
const uptime = seconds => `${seconds}s`;
const formatDateTime = seconds => String(seconds);
const action = (text, fn) => { const node=document.createElement('button'); node.textContent=text;
node.addEventListener('click',fn); return node; };
const el = (tag, text, cls) => {const node=document.createElement(tag);
if(text!==undefined) node.textContent=text; if(cls) node.className=cls; return node;};
const updateCacheMemorySizing = () => {};
const rustProxyRateSampling = {cadence:()=>3,difference:(previous,values,at)=>({next:{values,at},
deltas:previous?values.map((v,i)=>v-previous.values[i]):null,seconds:3})};
'''
with sync_playwright() as p:
browser = p.chromium.launch(headless=True, executable_path=shutil.which('chromium'), args=['--no-sandbox'])
ctx = browser.new_context(viewport={'width':393,'height':844}, device_scale_factor=3,
is_mobile=True, has_touch=True)
page = ctx.new_page()
page.set_content(html, wait_until='domcontentloaded')
page.on('pageerror', lambda error: print('BROWSER ERROR:', error))
page.add_script_tag(content=stubs + renderer)
result = page.evaluate('''() => {
const system = {source:'sysinfo', platform:'Linux', process_threads:5,
process_cpu_percent:5,process_rss_bytes:100,cpu_percent:15,
network_rx_bytes_per_second:20, network_tx_bytes_per_second:10,
sampled_at_ms:Date.now(),network_interfaces:[
{name:'eth0',rx_bytes_per_second:10,tx_bytes_per_second:5}]};
const s = {system,cache:{hits:50,misses:30,bypasses:10,resident_bytes:4096},
metrics:{http3_requests:2,mean_duration_ms:0.5},uptime_seconds:100,
listeners:{active_endpoints:[]},proxy_listen:null,tls_listen:null};
renderDiagnostics(s,true,true);
const process = document.querySelector('[data-dashboard-widget="process_diagnostics"]');
const network = document.querySelector('[data-dashboard-widget="network_diagnostics"]');
const target = document.getElementById('network-interface-charts');
const firstRow = target.firstElementChild;
const initialDetail = document.getElementById('process-cpu').textContent;
const initialHistory = processCpuSamples.length;
window.scrollTo(0, Math.round(network.getBoundingClientRect().top + window.scrollY + 125));
const y = window.scrollY, height = process.getBoundingClientRect().height;
for (let i=0; i<35; i++) {
s.live_system = {sampled_at_ms:Date.now(),process_cpu_percent:7+i,
process_rss_bytes:150+i, cpu_percent:22+i,
network_rx_bytes_per_second:100+i*20,network_tx_bytes_per_second:10+i,
network_interfaces:[{name:'eth0',rx_bytes_per_second:100+i*20,
tx_bytes_per_second:10+i}]};
renderDiagnostics(s,true,false);
}
const unchangedDetails = document.getElementById('process-cpu').textContent === initialDetail;
const recentChart = document.getElementById('process-cpu-chart-value').textContent;
const iface = target.querySelector('.network-interface-rate').textContent;
const beforeSystem = processCpuSamples.length;
const y2 = window.scrollY;
s.system = {...system, process_cpu_percent:93,process_rss_bytes:3000,
network_rx_bytes_per_second:9000, network_tx_bytes_per_second:900};
renderDiagnostics(s,false,true);
const slowlyUpdated = document.getElementById('process-cpu').textContent;
const pill = document.querySelector('.pill');
const origin = document.querySelector('.vhost-backend-protocol-badge');
const roundedOrigin = getComputedStyle(origin).borderTopLeftRadius;
document.documentElement.dataset.cornerStyle = 'default';
const defaultPillRadius = getComputedStyle(pill).borderTopLeftRadius;
const defaultOriginRadius = getComputedStyle(origin).borderTopLeftRadius;
document.documentElement.dataset.cornerStyle = 'rounded';
return {initialDetail,initialHistory,unchangedDetails,recentChart,iface,
beforeSystem,afterSystem:processCpuSamples.length,slowlyUpdated,
rowStable:target.firstElementChild===firstRow,
heightDelta:process.getBoundingClientRect().height-height,
scrollDelta:y2-y,postSystemScrollDelta:window.scrollY-y,
nestedScroll:getComputedStyle(target).overflowY,
nestedMaxHeight:getComputedStyle(target).maxHeight,
pillRadius:getComputedStyle(pill).borderTopLeftRadius,
originRadius:roundedOrigin,defaultPillRadius,defaultOriginRadius,
originColor:getComputedStyle(origin).color};
}''')
assert result['initialDetail'] == '5%', result
assert result['initialHistory'] == 1, result
assert result['unchangedDetails'] and result['recentChart'] == '41%', result
assert result['beforeSystem'] == 36 == result['afterSystem'], result
assert result['slowlyUpdated'] == '93%', result
assert result['rowStable'] and '780 B' in result['iface'], result
assert abs(result['heightDelta']) < 2 and abs(result['scrollDelta']) < 2, result
assert abs(result['postSystemScrollDelta']) < 2, result
assert result['nestedScroll'] == 'visible' and result['nestedMaxHeight'] == 'none', result
assert result['pillRadius'] == '8px' and result['originRadius'] == '8px', result
assert result['defaultPillRadius'] == result['defaultOriginRadius'], result
assert result['originColor'] != '', result
print('PASS: 35 live CPU/RSS/network samples, slow Process metadata, stable mobile layout/scroll, badge radii', result)
browser.close()