#!/usr/bin/env python3 """Simulated Chromium Vhost backend quick-view, without a Rust server. Verify both backend triggers, the shared click-to-open upstream H2 protocol popover without native title flicker, and the two Cache card margins. Exercise live updates, safe text, keyboard/outside dismissal and mobile sizing. """ from pathlib import Path from panel_html import render_admin_index from playwright.sync_api import sync_playwright import json import re import time 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() PACKS = {lang: {'id': lang, **json.loads((ROOT / 'langs' / f'{lang}.json').read_text())} for lang in ('en_US', 'pl_pl')} 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' / f'{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) CONFIG = { 'vhosts': [{ 'id': 'api-one', 'enabled': True, 'source': {'type': 'reverse_proxy'}, 'frontend': {'hosts': ['api.example.test'], 'listener_ids': []}, 'backends': [ {'id': 'origin-http', 'url': 'http://10.12.0.5:8080/api', 'protocol': 'http2', 'enabled': True}, {'id': '', 'url': 'https://hiddenuser:secret@backend.example.test:8443/path', 'protocol': 'auto', 'enabled': True}, ] }, {'id': 'static-only', 'enabled': True, 'source': {'type': 'web_server', 'web_service_id': 'static-test'}, 'frontend': {'hosts': ['static.example.test'], 'listener_ids': []}, 'backends': []}], 'listeners': {'endpoints': []}, 'web_services': [], 'acl_profiles': [], } def snapshot(first=200): return {'backend_pools': [{ 'id': 'api-one', 'health_mode': 'on', 'members': [ {'id': 'origin-http', 'status': 'up', 'last_status': first, 'requests': 12, 'active': 0, 'errors': 0, 'bytes_in': 0, 'bytes_out': 0}, {'id': '', 'status': 'down', 'last_status': 0, 'requests': 0, 'active': 0, 'errors': 0, 'bytes_in': 0, 'bytes_out': 0}, ] }], 'vhost_metrics': {}} def protocol_snapshot(h2_count): status = snapshot() status['backend_pools'][0]['members'][0].update({ 'upstream_protocol_observed': 'h2', 'upstream_protocol_last_seen_ms': int(time.time() * 1000), 'upstream_protocol_http1_total': 3, 'upstream_protocol_http2_total': h2_count, }) return status with sync_playwright() as playwright: browser = playwright.chromium.launch(headless=True, executable_path='/usr/bin/chromium', args=['--no-sandbox']) for width in (320, 375, 768, 1440): page = browser.new_page(viewport={'width': width, 'height': 820}) errors = [] page.on('pageerror', lambda error: errors.append(str(error))) 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()) page.evaluate('''([config, status, packs]) => { languagePacks = new Map(Object.entries(packs)); defaultLanguage = 'en_US'; language = 'pl_pl'; document.documentElement.lang = 'pl-PL'; document.documentElement.classList.remove('app-loading', 'app-startup-failed'); document.getElementById('login').hidden = true; document.getElementById('workspace').hidden = false; document.getElementById('panel-vhosts').hidden = false; currentPanel = 'vhosts'; envelope = {revision: 12, config}; latestStatus = status; window.rustProxyVhosts.render(); }''', [CONFIG, snapshot(), PACKS]) vhost = page.locator('[data-vhost-card="api-one"]') backend = vhost.locator('.vhost-detail.vhost-backend-trigger') health = vhost.locator('.vhost-live-metric.vhost-backend-trigger') assert backend.count() == health.count() == 1, width assert backend.get_attribute('aria-expanded') == 'false' assert backend.get_attribute('aria-haspopup') == 'dialog' # Clickable backend/health fields must match their non-button peers, # including for user-selected interface corner styles. shapes = page.evaluate('''() => { const root = document.querySelector('[data-vhost-card="api-one"]'); const peers = [ [root.querySelector('.vhost-detail.vhost-backend-trigger'), root.querySelector('.vhost-detail:not(.vhost-backend-trigger)')], [root.querySelector('.vhost-live-metric.vhost-backend-trigger'), root.querySelector('.vhost-live-metric:not(.vhost-backend-trigger)')] ]; const checks = ['square', 'subtle', 'rounded', 'extra'].map(corner => { document.documentElement.dataset.cornerStyle = corner; return {corner, tiles: peers.map(([trigger, peer]) => { const left = getComputedStyle(trigger), right = getComputedStyle(peer); return {radius: left.borderRadius, peerRadius: right.borderRadius, padding: left.padding, peerPadding: right.padding, font: left.fontFamily, peerFont: right.fontFamily}; })}; }); document.documentElement.removeAttribute('data-corner-style'); return checks; }''') for shape in shapes: for tile in shape['tiles']: assert tile['radius'] == tile['peerRadius'] == '0px', (width, shape) assert tile['padding'] == tile['peerPadding'], (width, shape) assert tile['font'] == tile['peerFont'], (width, shape) backend.click() popup = page.locator('#vhost-backend-popup') assert popup.count() == 1, width assert backend.get_attribute('aria-expanded') == 'true' assert popup.get_attribute('role') == 'dialog' # The anchored popup is a surface, not a permanently rounded tooltip. # It must follow live interface corner preference changes without # dismissing/recreating the open backend (or shared H2) popup. corners = page.evaluate('''() => { const node = document.getElementById('vhost-backend-popup'); const saved = document.documentElement.getAttribute('data-corner-style'); const out = {}; for (const style of ['default', 'square', 'subtle', 'rounded', 'extra']) { document.documentElement.dataset.cornerStyle = style; out[style] = { popup: getComputedStyle(node).borderRadius, row: getComputedStyle(node.querySelector('.vhost-backend-popup-row')).borderRadius, close: getComputedStyle(node.querySelector('.vhost-backend-popup-close')).borderRadius, reference: getComputedStyle(document.querySelector('[data-vhost-card="api-one"]')).borderRadius, sameNode: node === document.getElementById('vhost-backend-popup'), }; } if (saved === null) document.documentElement.removeAttribute('data-corner-style'); else document.documentElement.dataset.cornerStyle = saved; return out; }''') expected_radii = {'default': ('0px', '0px'), 'square': ('0px', '0px'), 'subtle': ('5px', '4px'), 'rounded': ('12px', '8px'), 'extra': ('18px', '12px')} for style, (surface, control) in expected_radii.items(): actual = corners[style] assert actual == {'popup': surface, 'row': surface, 'close': control, 'reference': surface, 'sameNode': True}, (width, style, actual) values = popup.evaluate('''node => ({ cards: node.querySelectorAll('.vhost-backend-popup-row').length, text: node.textContent, html: node.innerHTML, box: (() => { const r = node.getBoundingClientRect(); return {left: r.left, right: r.right, top:r.top, bottom:r.bottom}; })(), })''') assert values['cards'] == 2, (width, values) assert '10.12.0.5:8080' in values['text'] and 'HTTP 200' in values['text'], (width, values) assert 'backend.example.test:8443' in values['text'], width assert 'secret' not in values['text'], width assert '' in values['text'] and '<origin-dns>' in values['html'], width assert 'Brak odpowiedzi HTTP' in values['text'], width assert 'HTTPS · AUTO' in values['text'], width assert values['box']['left'] >= -1 and values['box']['right'] <= width + 1, (width, values['box']) assert values['box']['top'] >= -1 and values['box']['bottom'] <= 821, (width, values['box']) assert page.evaluate('document.documentElement.scrollWidth - innerWidth') <= 2, width page.evaluate('''s => window.rustProxyVhosts.renderStatus(s)''', snapshot(503)) assert 'HTTP 503' in popup.inner_text(), width assert popup.locator('.has-error').count() == 1, width health.click() assert popup.count() == 1 and health.get_attribute('aria-expanded') == 'true', width assert backend.get_attribute('aria-expanded') == 'false', width page.keyboard.press('Escape') assert popup.count() == 0 and health.get_attribute('aria-expanded') == 'false', width assert health.evaluate('(node) => document.activeElement === node'), width backend.click() page.locator('#vhost-search').click(force=True) assert popup.count() == 0, width # A static-only vhost presents a translated empty state, not stale pool values. page.locator('[data-vhost-card="static-only"] .vhost-detail.vhost-backend-trigger').click() assert 'Brak skonfigurowanych backendów.' in popup.inner_text(), width popup.locator('.vhost-backend-popup-close').click() assert popup.count() == 0, width backend.click() assert popup.count() == 1, width # The existing backend popover is also used by the H2 policy badge. protocol = vhost.locator('[data-upstream-protocol-backend="origin-http"]') assert protocol.count() == 1 and protocol.evaluate('n => n.tagName') == 'BUTTON', width assert protocol.get_attribute('title') is None, width protocol.click() assert backend.get_attribute('aria-expanded') == 'false', width assert protocol.get_attribute('aria-expanded') == 'true', width assert 'Brak odpowiedzi backendu' in popup.inner_text(), width page.evaluate('''s => window.rustProxyVhosts.renderStatus(s)''', protocol_snapshot(8)) assert 'origin-http · H2 compatible → H2' in protocol.inner_text(), width assert 'Ostatnia wersja HTTP' in popup.inner_text(), width assert popup.locator('.vhost-backend-popup-field > span:last-child').all_inner_texts() == ['3', '8'], width page.evaluate('''() => { window.__popupReference = document.getElementById('vhost-backend-popup'); window.__headingReference = window.__popupReference.querySelector('h4'); window.__popupRemoved = 0; window.__popupObserver = new MutationObserver(records => { for (const record of records) for (const item of record.removedNodes) { if (item === window.__popupReference) window.__popupRemoved++; } }); window.__popupObserver.observe(document.body, {childList: true}); }''') for count in range(9, 19): page.evaluate('''s => window.rustProxyVhosts.renderStatus(s)''', protocol_snapshot(count)) unchanged = page.evaluate('''() => ({ samePopup: document.getElementById('vhost-backend-popup') === window.__popupReference, sameHeading: window.__popupReference.querySelector('h4') === window.__headingReference, removed: window.__popupRemoved, expanded: document.querySelector('[data-upstream-protocol-backend="origin-http"]').getAttribute('aria-expanded'), })''') assert unchanged == {'samePopup': True, 'sameHeading': True, 'removed': 0, 'expanded': 'true'}, (width, unchanged) assert popup.locator('.vhost-backend-popup-field > span:last-child').all_inner_texts() == ['3', '18'], width page.evaluate('window.__popupObserver.disconnect()') assert protocol.get_attribute('title') is None, width # A stale observation should change the displayed information in # place, not dismiss/recreate the click popover. stale = protocol_snapshot(18) stale['backend_pools'][0]['members'][0]['upstream_protocol_last_seen_ms'] -= 600000 page.evaluate('''s => window.rustProxyVhosts.renderStatus(s)''', stale) assert 'H2 compatible → ?' in protocol.inner_text(), width assert popup.locator('.vhost-backend-popup-list').is_hidden(), width assert page.evaluate('document.getElementById("vhost-backend-popup") === window.__popupReference'), width page.keyboard.press('Escape') assert popup.count() == 0 and protocol.get_attribute('aria-expanded') == 'false', width assert protocol.evaluate('node => document.activeElement === node'), width # Cache Domains and Stored objects are the only directly adjacent # table cards and need a visible, small gap at any viewport width. cache_gap = page.evaluate('''() => { const panel = document.getElementById('panel-cache'); panel.hidden = false; const [domains, stored] = panel.querySelectorAll(':scope > article.card'); const gap = stored.getBoundingClientRect().top - domains.getBoundingClientRect().bottom; panel.hidden = true; return gap; }''') assert 9.5 <= cache_gap <= 10.5, (width, cache_gap) page.evaluate('window.rustProxyVhosts.reset()') assert popup.count() == 0, width assert not errors, (width, errors) print(f'PASS Vhosts shared backend/H2 popover and Cache gap at {width}px') page.close() browser.close() print('PASS Vhost quick-view with simulated runtime status (not live Rust)')