Files
RustProxy/scripts/tests/web_server_0334_browser_test.py
T
2026-10-16 13:02:53 +02:00

156 lines
9.1 KiB
Python
Executable File

#!/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
from panel_html import render_admin_index
import json
import re
from playwright.sync_api import sync_playwright
ROOT = Path(__file__).resolve().parents[2]
HTML = render_admin_index(ROOT)
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)')