#!/usr/bin/env python3 """Regression for live Network diagnostic DOM stability beside Process. Runs the actual renderer and the shipped dashboard CSS with mocked sample data in Chromium touch emulation. This does NOT certify Safari/iOS 27 behavior. """ 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' 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 source = (WEB / 'app/runtime.js').read_text() start = source.index('function renderNetworkInterfaceCharts(interfaces, appendSample) {') start = source.rfind('let latestNetworkInterfaceSnapshot', 0, start) assert start >= 0 renderer = source[start:source.index('function renderDiagnostics(', start)] helper = source[source.index('function setChartText('):start] html = ('' '' '' '
Overview metrics
' '
' + str(process) + str(network) + '
') stubs = ''' const networkInterfaceSamples = new Map(); const hiddenNetworkInterfaces = new Set(); const $ = id => document.getElementById(id); const t = s => ({networkInterfacesVisibleCount:'{visible} / {total}', networkInterfaceRate:'Rate', networkInterfacesHistory:'Traffic', hideMetric:'Hide', showMetric:'Show',hiddenInterfaces:'Hidden', networkNoInterfaces:'No interfaces',networkNoVisibleInterfaces:'None visible'})[s] || s; const fmtBytes = n => `${n} B`; const el = (tag, text, cls) => {const node=document.createElement(tag); if(text !== undefined) node.textContent=text;if(cls) node.className=cls;return node;}; const action = (label, fn) => {const node=el('button',label);node.addEventListener('click',fn);return node;}; const svgNode = (tag, attrs={}) => {const node=document.createElementNS('http://www.w3.org/2000/svg',tag); Object.entries(attrs).forEach(([key,value])=>node.setAttribute(key,value));return node;}; const sparklinePoints = (values,width=300,height=60,padding=5,peakOverride=null) => { const peak=Math.max(1,peakOverride??Math.max(0,...values)); return values.map((value,index)=>`${index*width/Math.max(1,values.length-1)},${height-padding-Math.max(0,value)/peak*(height-padding*2)}`).join(' '); }; const saveHiddenNetworkInterfaces = () => {}; ''' with sync_playwright() as p: browser = p.chromium.launch(headless=True, executable_path=shutil.which('chromium'), args=['--no-sandbox']) context = browser.new_context(viewport={'width':393,'height':844}, device_scale_factor=3, is_mobile=True, has_touch=True) page = context.new_page() page.set_content(html, wait_until='domcontentloaded') page.add_script_tag(content=stubs + helper + renderer) baseline = page.evaluate('''() => { const items = [{name:'ens18',rx_bytes_per_second:100,tx_bytes_per_second:20}, {name:'lo',rx_bytes_per_second:3,tx_bytes_per_second:3}]; renderNetworkInterfaceCharts(items,true); const panel = document.querySelector('[data-dashboard-widget="process_diagnostics"]'); const network = document.querySelector('[data-dashboard-widget="network_diagnostics"]'); const target = document.getElementById('network-interface-charts'); window.testNodes = {first:target.firstElementChild, svg:target.querySelector('svg'), path:target.querySelector('.network-rx-line')}; window.testInterfaces = items; window.scrollTo(0,Math.round(network.getBoundingClientRect().top+window.scrollY+180)); return {processHeight:panel.getBoundingClientRect().height, y:scrollY, network:network.getBoundingClientRect().top, count:target.children.length, scrollHeight:document.documentElement.scrollHeight}; }''') assert baseline['y'] > 0 and baseline['count'] == 2, baseline count = page.evaluate('''() => { const target = document.getElementById('network-interface-charts'); let mutations=0; const obs = new MutationObserver(list=>mutations+=list.filter(item=>item.type==='childList').length); obs.observe(target,{childList:true,subtree:true}); const beforeY=scrollY; const beforeHeight=document.documentElement.scrollHeight; const beforeProcess=document.querySelector('[data-dashboard-widget="process_diagnostics"]').getBoundingClientRect().height; for(let i=0;i<45;i++){ renderNetworkInterfaceCharts([{name:'ens18',rx_bytes_per_second:i*109,tx_bytes_per_second:12}, {name:'lo',rx_bytes_per_second:i*2,tx_bytes_per_second:3}],true); if(target.firstElementChild!==window.testNodes.first || target.querySelector('svg')!==window.testNodes.svg || target.querySelector('.network-rx-line')!==window.testNodes.path) throw Error('Network DOM replaced'); } obs.takeRecords().forEach(r=> {if(r.type==='childList')mutations++});obs.disconnect(); return {mutations, y:scrollY, beforeY, count:target.children.length, scrollHeight:document.documentElement.scrollHeight, beforeHeight, processHeight:document.querySelector('[data-dashboard-widget="process_diagnostics"]').getBoundingClientRect().height, beforeProcess, changed:target.querySelector('.network-interface-rate').textContent.includes('4796')}; }''') assert count['mutations'] == 0, count assert abs(count['y'] - count['beforeY']) < 2, count assert count['count'] == 2 and count['changed'], count assert abs(count['scrollHeight'] - count['beforeHeight']) < 2, count assert abs(count['processHeight'] - count['beforeProcess']) < 2, count print('PASS: 45 updates preserve SVG nodes, Process height, document height and scroll') # Interface charts advance at every Overview live tick like the aggregate # network chart, repeating the last observed rate until system-info updates. # A system-info-only refresh or Hide/Show redraw never adds history points. history = page.evaluate('''() => { networkInterfaceSamples.clear(); const one=[{name:'ens18',rx_bytes_per_second:100,tx_bytes_per_second:200}]; renderNetworkInterfaceCharts(one,true); const before=networkInterfaceSamples.get('ens18').rx.length; for(let i=0;i<45;i++) renderNetworkInterfaceCharts(one,true); const live=networkInterfaceSamples.get('ens18').rx.length; const measured=[{name:'ens18',rx_bytes_per_second:500,tx_bytes_per_second:250}]; renderNetworkInterfaceCharts(measured,false); // only new system-info data const afterSystem=networkInterfaceSamples.get('ens18').rx.length; const label=document.querySelector('.network-interface-rate').textContent; renderNetworkInterfaceCharts(measured,true); // next live tick const afterLive=networkInterfaceSamples.get('ens18').rx.length; const newest=networkInterfaceSamples.get('ens18').rx.at(-1); return {before,live,afterSystem,afterLive,label,newest}; }''') assert history['before'] == 1 and history['live'] == 46, history assert history['afterSystem'] == 46 and history['afterLive'] == 47, history assert history['newest'] == 500 and '500 B' in history['label'], history assert 'renderNetworkInterfaceCharts(live.network_interfaces || [], appendSample);' in source refresh = (WEB / 'app/refresh.js').read_text() assert 'interval: () => options.system_info_seconds' in refresh assert 'renderDiagnostics(latestStatus, false, true);' in refresh print('PASS: frequent interface chart history, configured system-info cadence, no extra samples on system-only redraw') page.evaluate('''() => renderNetworkInterfaceCharts([ {name:'ens18',rx_bytes_per_second:100,tx_bytes_per_second:200}, {name:'lo',rx_bytes_per_second:3,tx_bytes_per_second:3}],false)''') # The Hide / Show controls retain the newest sample on click, not the array # captured when a chart node was created. page.locator('.network-interface-hide').first.click() assert page.locator('.network-interface-row').count() == 1 assert page.locator('.network-interface-hidden button').count() == 1 page.locator('.network-interface-hidden button').click() assert page.locator('.network-interface-row').count() == 2 assert page.locator('.network-interface-hidden').count() == 0 page.evaluate('''() => renderNetworkInterfaceCharts([{name:'eth0',rx_bytes_per_second:9,tx_bytes_per_second:11}],true)''') assert page.locator('.network-interface-row').count() == 1 assert page.locator('.network-interface-row').first.get_attribute('data-interface-name') == 'eth0' page.evaluate('''() => renderNetworkInterfaceCharts([],true)''') assert page.locator('.network-interface-empty').count() == 1 print('PASS: Hide/Show actions, hotplug/removal and empty state still work') browser.close()