#!/usr/bin/env python3 """WWW/Vhost visual parity in production UI files, using simulated status. Chromium exercises the real assembled admin JS/CSS and responsive layouts. It does not establish Rust, SSE or PHP-FPM runtime acceptance. """ from pathlib import Path import json import re from playwright.sync_api import sync_playwright ROOT = Path(__file__).resolve().parents[2] HTML = (ROOT / 'src/admin/web/index.html').read_text() CSS = (ROOT / 'src/admin/web/style.css').read_text() ROUTER = (ROOT / 'src/admin/router.rs').read_text() def app_bundle(): found = re.search(r'fn admin_app_javascript\([^)]*\)[^{]*\{', ROUTER) assert found code = ROUTER[found.end():] code = code[:re.search(r'\]\s*\.join\("\\n"\)', code).start()] fragments = [] for line in code.splitlines(): line = line.strip() include = re.search(r'include_str!\("([^"]+)"\)', line) if include: fragments.append((ROOT / 'src/admin' / include.group(1)).read_text()) elif line == 'admin_vhosts_javascript(),': fragments.append('(() => {\n' + '\n'.join( (ROOT / 'src/admin/web/vhosts' / (name + '.js')).read_text() for name in ('core', 'protocol', 'health', 'rates', 'editor', 'render')) + '\n})();') elif line.startswith('"') and line.endswith('",'): fragments.append(json.loads(line[:-1])) return '\n'.join(fragments) def status(step): return {'web_server_metrics': { 'site-A': {'web': { 'requests': step * 120, 'response_headers_us': step * 2_400_000, 'status_5xx': step * 4, 'response_bytes': step * 20_480, 'static_requests': step * 80, 'php_requests': step * 30, 'other_requests': step * 10, }, 'php': {'capacity_used': 3, 'capacity_limit': 16}}, 'site-B': {'web': { 'requests': step * 24, 'response_headers_us': step * 100_000, 'status_5xx': 0, 'response_bytes': step * 8_192, 'static_requests': step * 24, 'php_requests': 0, 'other_requests': 0, }, 'php': None}, }} with sync_playwright() as playwright: browser = playwright.chromium.launch( headless=True, executable_path='/usr/bin/chromium', args=['--no-sandbox']) for width in (320, 375, 640, 800, 980, 1440, 1920): page = browser.new_page(viewport={'width': width, 'height': 900}) errors = [] page.on('pageerror', lambda e: errors.append(str(e))) 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=app_bundle()) initial = page.evaluate('''(status) => { document.documentElement.classList.remove('app-loading', 'app-startup-failed'); document.getElementById('login').hidden = true; document.getElementById('workspace').hidden = false; document.getElementById('panel-web-server').hidden = false; currentPanel = 'web-server'; envelope = {revision: 10, config: {web_services: [ {id: 'site-A', name: 'Example service', document_root: '/var/www/a', index: ['index.html'], php: {enabled: true}}, {id: 'site-B', name: 'Static service', document_root: '/var/www/b', index: ['index.html']} ], vhosts: [], php_fpm_profiles: []}}; window.__clock = 1000; Object.defineProperty(performance, 'now', {configurable: true, value: () => window.__clock}); latestStatus = status; window.rustProxyWebServer.render(); const list = document.querySelector('#web-service-list'); const cards = [...list.querySelectorAll(':scope > .vhost-card')]; const metricPanels = cards.map(card => card.querySelector('.www-service-panel')); const grid = metricPanels[0].querySelector('.vhost-live-grid'); const chart = metricPanels[0].querySelector('.vhost-chart'); const svg = chart.querySelector('svg'); const svgBox = svg.getBoundingClientRect(), svgMatrix = svg.getScreenCTM(); const rect = node => node.getBoundingClientRect(); const expectedMetricLabels = ['webLiveRequests','webLiveRate','webLiveErrors', 'webLiveLatency','webLivePhpCapacity','webLiveBytes']; return { cards: cards.length, sectionTags: metricPanels.map(panel => panel.tagName), collapsibles: list.querySelectorAll('details, summary').length, liveFields: metricPanels.map(panel => panel.querySelectorAll('[data-web-live-field]').length), labels: [...grid.children].map(tile => tile.querySelector('span')?.dataset.i18n), tileClasses: [...grid.children].map(tile => tile.className), values: [...grid.querySelectorAll('strong')].map(el => el.textContent), phpMissing: metricPanels[1].querySelector('[data-web-live-field="php"]').textContent, columns: getComputedStyle(grid).gridTemplateColumns.split(' ').length, chartWidth: rect(chart).width, gridWidth: rect(grid).width, cardWidth: rect(cards[0]).width, listWidth: rect(list).width, svgWidth: svgBox.width, svgContentWidth: svg.viewBox.baseVal.width * Math.hypot(svgMatrix.a, svgMatrix.b), preserveAspect: svg.getAttribute('preserveAspectRatio'), stroke: getComputedStyle(svg.querySelector('polyline')).vectorEffect, chartLabel: !!chart.querySelector('.vhost-chart-head .small-label'), rendered: getComputedStyle(metricPanels[0]).display !== 'none' && rect(chart).height > 60, fitsViewport: document.documentElement.scrollWidth <= innerWidth + 2, expectedMetricLabels, }; }''', status(1)) expected_cols = 2 if width <= 640 else 3 if width <= 980 else 6 assert initial['cards'] == 2, (width, initial) assert initial['sectionTags'] == ['SECTION', 'SECTION'], (width, initial) assert initial['collapsibles'] == 0, (width, initial) assert initial['liveFields'] == [7, 7], (width, initial) assert initial['labels'] == initial['expectedMetricLabels'], (width, initial) assert initial['tileClasses'] == ['vhost-live-metric'] * 6, (width, initial) assert initial['values'][0] != '-' and initial['values'][4] == '3 / 16', (width, initial) assert initial['phpMissing'] != '-', (width, initial) assert initial['columns'] == expected_cols, (width, initial) assert initial['chartWidth'] > 0.95 * initial['gridWidth'], (width, initial) assert initial['listWidth'] - initial['cardWidth'] < 3, (width, initial) assert abs(initial['svgWidth'] - initial['svgContentWidth']) <= 2, (width, initial) assert initial['preserveAspect'] == 'none' and initial['stroke'] == 'non-scaling-stroke', (width, initial) assert initial['chartLabel'] and initial['rendered'] and initial['fitsViewport'], (width, initial) sampled = page.evaluate('''([first, second, last]) => { const api = window.rustProxyWebServer; const panel = document.querySelector('[data-web-live-id="site-A"]'); const rate = panel.querySelector('[data-web-live-field="rate"]'); window.__clock = 4000; api.renderLive(first, true); window.__clock = 7000; api.renderLive(second, true); const before = {rate: rate.textContent, points: panel.querySelector('polyline').getAttribute('points')}; // Service-list redraw must show the current status without opening anything. latestStatus = last; api.render(); const updated = document.querySelector('[data-web-live-id="site-A"]'); const total = updated.querySelector('[data-web-live-field="requests"]').textContent; const split = updated.querySelector('[data-web-live-field="split"]').textContent; return {before, total, split, pointsAfterRedraw: updated.querySelector('polyline').getAttribute('points'), metricsRemainVisible: !!updated.querySelector('.vhost-live-metric'), overflow: document.documentElement.scrollWidth - innerWidth}; }''', [status(2), status(3), status(4)]) assert sampled['before']['rate'] not in ('-', '0/s', '0.0/s'), (width, sampled) assert sampled['before']['points'].count(' ') >= 1, (width, sampled) assert sampled['total'] != initial['values'][0], (width, sampled) assert sampled['split'] != '-' and sampled['metricsRemainVisible'], (width, sampled) assert sampled['pointsAfterRedraw'] == sampled['before']['points'], (width, sampled) assert sampled['overflow'] <= 2, (width, sampled) assert not errors, (width, errors) print(f'PASS 0.33.4 WWW live metrics visual parity at {width}px; {expected_cols} columns') page.close() browser.close() print('PASS always-visible full-width WWW/Vhost parity with simulated updates (not Rust runtime)')