#!/usr/bin/env python3 """0.33.1 factory/default + unsaved layout regression against the full bundled JS. The authenticated app bundle runs against simulated status/preferences; this is not a Rust server integration test. """ import json import re from pathlib import Path from panel_html import render_admin_index from playwright.sync_api import sync_playwright ROOT = Path(__file__).resolve().parents[2] PRESET = json.loads((ROOT / 'src/admin/web/dashboard-defaults.json').read_text()) REGISTRY = json.loads((ROOT / 'src/admin/web/dashboard-widgets.json').read_text()) ROUTER = (ROOT / 'src/admin/router.rs').read_text() def app_bundle(): match = re.search(r'fn admin_app_javascript\([^)]*\)[^{]*\{', ROUTER) assert match body = ROUTER[match.end():] body = body[:re.search(r'\]\s*\.join\("\\n"\)', body).start()] result = [] for line in body.splitlines(): line = line.strip() include = re.search(r'include_str!\("([^"]+)"\)', line) if include: result.append((ROOT / 'src/admin' / include.group(1)).read_text()) elif line == 'admin_vhosts_javascript(),': result.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('",'): result.append(json.loads(line[:-1])) return '\n'.join(result) assert len(PRESET['dashboard_widgets']) == len(REGISTRY) assert set(PRESET['dashboard_widgets']) == {w['id'] for w in REGISTRY} assert len(set(PRESET['dashboard_hidden_widgets'])) == len(PRESET['dashboard_hidden_widgets']) assert set(PRESET['dashboard_hidden_widgets']).issubset(set(PRESET['dashboard_widgets'])) assert PRESET['dashboard_density'] == 'compact' and PRESET['overview_samples'] == 600 assert sum(w['id'].startswith('www_') and w['id'] in PRESET['dashboard_hidden_widgets'] for w in REGISTRY) == 12 HTML = render_admin_index(ROOT) CSS = (ROOT / 'src/admin/web/style.css').read_text() BUNDLE = app_bundle() assert BUNDLE.index('const ADMIN_DASHBOARD_DEFAULTS =') < BUNDLE.index('window.rustProxyOverviewSamples =') def load(page): 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_function('window.rustProxyInlineLayout && window.rustProxyPreferences && window.rustProxyBackendDashboard') def inspect(page, ids): return page.evaluate('''ids => Object.fromEntries(ids.map(id => { const node = document.querySelector(`[data-dashboard-widget="${id}"]`); return [id, {hidden:node.hidden, display:getComputedStyle(node).display}]; }))''', ids) def assert_visible(page, visible, hidden): result = inspect(page, visible + hidden) for widget in visible: assert result[widget]['hidden'] is False and result[widget]['display'] != 'none', (widget, result) for widget in hidden: assert result[widget]['hidden'] is True and result[widget]['display'] == 'none', (widget, result) with sync_playwright() as p: browser = p.chromium.launch(headless=True, executable_path='/usr/bin/chromium', args=['--no-sandbox']) for width in [320, 375, 800, 1440]: context = browser.new_context(viewport={'width': width, 'height': 950}) page = context.new_page() errors = [] page.on('pageerror', lambda e: errors.append(str(e))) load(page) result = page.evaluate('''() => { const prefs = ADMIN_APPEARANCE_DEFAULTS; document.documentElement.classList.remove('app-loading', 'app-startup-failed'); document.getElementById('workspace').hidden = false; document.getElementById('panel-overview').hidden = false; document.getElementById('overview-layout-unlock').hidden = false; currentPanel = 'overview'; window.__savedDashboard = JSON.parse(JSON.stringify(prefs)); window.rustProxyPreferences.load = async () => window.__savedDashboard; window.rustProxyPreferences.current = () => window.__savedDashboard; window.rustProxyPreferences.update = async patch => { window.__savedDashboard = {...window.__savedDashboard, ...patch}; applyAdminPreferences(window.__savedDashboard); return window.__savedDashboard; }; applyAdminPreferences(window.__savedDashboard); api = async path => { if (path === '/api/dashboard-presets') return []; throw new Error('unexpected API ' + path); }; appConfirm = async () => true; return { order: ADMIN_DASHBOARD_WIDGETS, hidden: ADMIN_DEFAULT_HIDDEN_WIDGETS, density: document.documentElement.dataset.dashboardDensity, samples:window.rustProxyOverviewSamples.getLimit(), serviceCount:document.querySelectorAll('#dashboard-overview-grid [data-dashboard-widget]').length, }; }''') assert result['order'] == PRESET['dashboard_widgets'], (width, result['order']) assert result['hidden'] == PRESET['dashboard_hidden_widgets'], (width, result['hidden']) assert result['samples'] == 600 and result['density'] == 'compact' and result['serviceCount'] == 78, result assert_visible(page, ['backend_traffic', 'extra_response_latency', 'wide_transform_activity'], ['backend_health', 'stream_sessions', 'www_status', 'bandwidth']) # Reproduce the previously flaky operation: opt in hidden backend + # Stream cards and opt out a visible backend card, WITHOUT saving yet. page.locator('#overview-layout-unlock').click() page.wait_for_function('window.rustProxyInlineLayout.isEditing()') outcome = page.evaluate('''() => { const add = id => { const label = dashboardWidgetLabel(id); const item = [...document.querySelectorAll('#overview-inline-hidden-list .overview-inline-hidden-item')] .find(node => node.firstElementChild.textContent === label); if (!item) throw new Error('no hidden card to restore: ' + id); item.querySelector('button').click(); }; add('backend_health'); add('stream_sessions'); const card = document.querySelector('[data-dashboard-widget="backend_traffic"]'); card.querySelector('.overview-inline-hide').click(); return window.rustProxyInlineLayout.currentDraft(); }''') assert 'backend_traffic' in outcome['hidden'] and 'backend_health' not in outcome['hidden'] assert 'stream_sessions' not in outcome['hidden'] assert_visible(page, ['backend_health', 'stream_sessions'], ['backend_traffic']) for _ in range(5): page.evaluate('''() => { const status = {revision:4,backend_pools:[],streams:{active:[],desired:[],metrics:{}}}; window.rustProxyBackendDashboard.render(status); window.rustProxyStreamDashboard.render(status); }''') assert_visible(page, ['backend_health', 'stream_sessions'], ['backend_traffic']) # Unrelated settings update must not repaint the saved layout over the draft. page.evaluate('''() => applyAdminPreferences({...window.__savedDashboard, ui_scale:105})''') assert_visible(page, ['backend_health', 'stream_sessions'], ['backend_traffic']) assert page.evaluate('window.rustProxyInlineLayout.isEditing()') page.locator('#overview-inline-cancel').click() assert not page.evaluate('window.rustProxyInlineLayout.isEditing()') assert_visible(page, ['backend_traffic'], ['backend_health', 'stream_sessions']) # Save under live traffic, ensure the in-flight preference application # uses the current draft before the editor exits. page.locator('#overview-layout-unlock').click() page.wait_for_function('window.rustProxyInlineLayout.isEditing()') page.evaluate('''() => { const label = dashboardWidgetLabel('backend_health'); const item = [...document.querySelectorAll('#overview-inline-hidden-list .overview-inline-hidden-item')] .find(node => node.firstElementChild.textContent === label); item.querySelector('button').click(); document.querySelector('[data-dashboard-widget="backend_traffic"] .overview-inline-hide').click(); }''') page.locator('#overview-inline-save').click() page.wait_for_function('!window.rustProxyInlineLayout.isEditing()') assert_visible(page, ['backend_health'], ['backend_traffic']) saved = page.evaluate('''() => window.__savedDashboard.dashboard_hidden_widgets''') assert 'backend_traffic' in saved and 'backend_health' not in saved page.evaluate('''() => { const status={backend_pools:[],streams:{active:[],desired:[],metrics:{}}}; window.rustProxyBackendDashboard.render(status); window.rustProxyStreamDashboard.render(status); }''') assert_visible(page, ['backend_health'], ['backend_traffic']) # Restore factory layout through the new named-presets modal. page.locator('#overview-layout-open').evaluate('(node) => node.hidden = false') page.locator('#overview-layout-open').click() page.wait_for_function("document.getElementById('overview-presets-dialog').open") page.locator('#overview-presets-factory').click() page.wait_for_function("!document.getElementById('overview-presets-dialog').open") assert_visible(page, ['backend_traffic'], ['backend_health', 'stream_sessions', 'www_status']) assert page.evaluate('window.__savedDashboard.dashboard_widgets') == PRESET['dashboard_widgets'] assert page.evaluate('window.__savedDashboard.dashboard_hidden_widgets') == PRESET['dashboard_hidden_widgets'] assert page.evaluate('document.documentElement.scrollWidth - window.innerWidth') <= 2 assert not errors, (width, errors) context.close() print('PASS inline layout defaults, add/hide, live ticks, other preferences, cancel/save', width) # An already-persisted sample choice must override the factory 600. context = browser.new_context() page = context.new_page() # This sandbox blocks all network origins; stub browser storage before # loading the bundle rather than navigating to a real host. page.evaluate("""() => Object.defineProperty(window, 'localStorage', { configurable: true, value: {getItem: key => key === 'rustproxy.overview.liveChartSamples' ? '120' : null, setItem: () => undefined}, })""") load(page) assert page.evaluate('window.rustProxyOverviewSamples.getLimit()') == 120 context.close() browser.close() print('PASS RustProxy 0.33.1 simulated browser layout regression')