156 lines
9.1 KiB
Python
Executable File
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)')
|