134 lines
8.2 KiB
Python
Executable File
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()
|