from pathlib import Path from panel_html import render_admin_index import re, json from playwright.sync_api import sync_playwright root=Path(__file__).resolve().parents[2] router=(root/'src/admin/router.rs').read_text() def concatenate(section): pattern=r'fn '+section+r'\([^)]*\)[^{]*\{' m=re.search(pattern,router) assert m, section text=router[m.end():] # The array contents are bounded by ].join("\\n") before the next function. text=text[:re.search(r'\]\s*\.join\("\\n"\)', text).start()] result=[] for line in text.splitlines(): line=line.strip() if 'include_str!("' in line: path=re.search(r'include_str!\("([^"]+)"\)',line).group(1) result.append((root/'src/admin'/path).read_text()) elif line=='admin_vhosts_javascript(),': result.append('(() => {\n'+''.join((root/'src/admin/web/vhosts'/f'{part}.js').read_text()+'\n' for part in ['core','protocol','health','rates','editor','render'])+'})();') elif line.startswith('"') and line.endswith('",'): result.append(json.loads(line[:-1])) return '\n'.join(result) bundle=concatenate('admin_app_javascript') assert 'window.rustProxyOverviewSamples =' in bundle assert bundle.index('window.rustProxyOverviewSamples =') < bundle.index('window.rustProxyOverviewSamples.onChange(') css=(root/'src/admin/web/style.css').read_text() html=render_admin_index(root) with sync_playwright() as p: browser=p.chromium.launch(headless=True, executable_path='/usr/bin/chromium',args=['--no-sandbox','--allow-file-access-from-files']) for width in [320,375,800,1440]: page=browser.new_page(viewport={'width':width,'height':900}) errors=[] page.on('pageerror',lambda e: errors.append(str(e))) # Execute in a fully populated DOM without disallowed filesystem/network navigation. page.set_content(html.replace('/assets/app___RUSTPROXY_ASSET_ID__.js','').replace('/assets/style___RUSTPROXY_ASSET_ID__.css','')) page.add_style_tag(content=css) page.add_script_tag(content=bundle) page.wait_for_timeout(550) state=page.evaluate('''() => ({ controller:typeof window.rustProxyOverviewSamples, handle:typeof window.rustProxyOverviewSamples?.onChange, web:typeof window.rustProxyWebOverview, services:typeof window.rustProxyWebServer, registry: document.querySelectorAll('[data-dashboard-widget^="www_"]').length, htmlWidth:document.documentElement.scrollWidth,windowWidth:window.innerWidth })''') print(width,state,'errors',errors) assert state['controller']=='object' and state['handle']=='function' and state['web']=='object' assert state['registry']==12 assert not [e for e in errors if 'onChange' in e or 'ReferenceError' in e or 'SyntaxError' in e or 'TypeError' in e],errors assert state['htmlWidth'] <= state['windowWidth'] + 2 # The selector belongs to the same header row as the core connection; # hiding it on other panels must not leave an empty dashboard row. layout = page.evaluate('''() => { document.getElementById('workspace').hidden = false; setPanel('overview', false); const status = document.getElementById('connection-label'); const sample = document.getElementById('overview-samples-toolbar'); const row = document.querySelector('.page-intro-meta-row'); const rect = node => { const r = node.getBoundingClientRect(); return {x:r.x,y:r.y,width:r.width,height:r.height,right:r.right}; }; const overview = { status:rect(status),sample:rect(sample),row:rect(row), visible: !sample.hidden,inside: row.contains(sample) && row.contains(status), overflow: document.documentElement.scrollWidth > innerWidth + 2 }; setPanel('web-server', false); const notOverview = sample.hidden && !status.hidden; setPanel('overview', false); return {overview,notOverview,visibleAgain:!sample.hidden}; }''') assert layout['overview']['inside'] and layout['overview']['visible'] assert layout['notOverview'] and layout['visibleAgain'] assert not layout['overview']['overflow'], (width, layout) if width >= 800: assert abs(layout['overview']['sample']['y'] - layout['overview']['status']['y']) <= 20, (width, layout) assert layout['overview']['sample']['right'] <= layout['overview']['row']['right'] + 2, (width, layout) # A genuinely sampled WWW metrics payload (not a screenshot-only fixture). report = page.evaluate('''() => { Object.defineProperty(window.performance, "now", {configurable:true, value:()=>window.__clock}); window.__clock = 1000; currentPanel = "overview"; document.getElementById('workspace').hidden = false; document.getElementById('login').hidden = true; document.getElementById('panel-overview').hidden = false; for (const n of document.querySelectorAll('[data-dashboard-widget^="www_"]')) n.hidden = false; function snap(k) { return {revision:7, web_server_metrics:{ "public-site": {web: {requests:100+k*30, status_2xx:86+k*24, status_3xx:3+k, status_4xx:8+k*2, status_5xx:3+k*3, response_headers_us:4000000+k*1300000, response_bytes:100000+k*20000, static_requests:70+k*20, php_requests:25+k*7, other_requests:5+k*3, methods:[90+k*25,8+k*4,0,0,2+k], selected_status:[80+k*18,2,2,1,5+k,0,0,0], active_headers:0}, php: {metrics:{requests:25+k*7, response_headers_us:2000000+k*700000, response_bytes:30000+k*5000,failures:[1+k,1+k,1+k,0,0,0,0,0,0]}, capacity_limit:32, capacity_used:8, capacity_available:24}}} }; } const before = snap(0), after = snap(1); // Saved custom layout from before 0.33.0 must not enable new cards. const savedLayout = applyDashboardLayout(['requests', 'errors'], normalizeHiddenDashboardWidgets([], ['requests', 'errors'])); const newCardsHidden = [...document.querySelectorAll('[data-dashboard-widget^="www_"]')] .every(card=>card.hidden); for (const n of document.querySelectorAll('[data-dashboard-widget^="www_"]')) n.hidden = false; window.rustProxyWebOverview.render(before, true); window.__clock = 4000; window.rustProxyWebOverview.render(after, true); let ids = ['www_requests-all-line', 'www_requests-php-line', 'www_latency-web-line', 'www_traffic-php-line', 'www_status_trend-5xx-line']; const linePaths = ids.map(id=>document.getElementById(id)?.getAttribute('d')); const statusText = document.getElementById('www_status-value').textContent; const capacityText = document.getElementById('www_capacity-value').textContent; const rateText = document.getElementById('www_php_requests-value').textContent; const bars = document.querySelectorAll('#www_top_services-bars .www-top-row').length; window.rustProxyOverviewSamples.setLimit(120); const historyLimit = window.rustProxyOverviewSamples.getLimit(); currentPanel = "web-server"; envelope = {config:{web_services:[{id:'public-site', name:'Public site',document_root:'/srv/www/public'}],vhosts:[]}}; window.rustProxyWebServer.render(); const details = document.querySelector('.www-service-panel'); if (!details || details.tagName !== 'SECTION') throw Error('Missing always-visible WWW metrics'); window.__clock = 7000; window.rustProxyWebServer.renderLive(before, true); window.__clock = 10000; window.rustProxyWebServer.renderLive(after, true); const value = details.querySelector('[data-web-live-field="requests"]')?.textContent; const phpCapacity = details.querySelector('[data-web-live-field="php"]')?.textContent; return {linePaths,statusText,capacityText,rateText,bars,historyLimit,value,phpCapacity,newCardsHidden, width:document.documentElement.scrollWidth}; }''') print('sampled WWW',width,report) assert all(report['linePaths']), report assert report['statusText'] != '-' and report['capacityText'] == '8 / 32', report assert report['rateText'] not in ('-', '0.0/s'), report assert report['bars'] == 1 and report['historyLimit'] == 120 and report['newCardsHidden'], report assert report['value'] and report['phpCapacity'] == '8 / 32', report assert report['width'] <= width + 2, report assert not [e for e in errors if any(x in e for x in ('ReferenceError', 'TypeError', 'SyntaxError'))], errors page.close() browser.close() print('PASS: actual composed admin bundle starts without a JS TypeError')