#!/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) # Test actual production CSS/JS in a browser with simulated preferences/status. 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, 1920]: context = browser.new_context(viewport={'width': width, 'height': 1000}) page = context.new_page() errors = [] page.on('pageerror', lambda error: errors.append(str(error))) load(page) page.evaluate('''() => { 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; document.getElementById('overview-layout-open').hidden = false; currentPanel = 'overview'; window.__savedDashboard = structuredClone(ADMIN_APPEARANCE_DEFAULTS); 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; }''') # Unchanged previous accounts preserve historic grouped rows. assert page.evaluate('''() => ({ legacy: document.getElementById('dashboard-overview-grid').dataset.overviewLayoutMode, widths:window.__savedDashboard.dashboard_widget_sizes, })''') == {'legacy':'legacy','widths':None} page.locator('#overview-layout-unlock').click() page.wait_for_function('window.rustProxyInlineLayout.isEditing()') # Widen a number+sparkline card from 2 to 3 and then 4 columns. selector = '[data-dashboard-widget="http_protocol"]' old_width = page.locator(selector).evaluate('(node) => node.getBoundingClientRect().width') page.locator(selector + ' .overview-inline-widen').click() assert page.locator(selector).get_attribute('data-dashboard-span') == '3' page.locator(selector + ' .overview-inline-widen').click() assert page.locator(selector).get_attribute('data-dashboard-span') == '4' assert page.locator(selector).evaluate('(node) => node.getBoundingClientRect().width') >= old_width * 1.7 assert page.evaluate('window.rustProxyInlineLayout.currentDraft().sizes.http_protocol') == 4 # Minimize a large multi-series chart to the new 3-column option. for _ in range(2): page.locator('[data-dashboard-widget="wide_transform_activity"] .overview-inline-narrow').click() assert page.locator('[data-dashboard-widget="wide_transform_activity"]').get_attribute('data-dashboard-span') == '3' # Real UI controls allow crossing old groups: send mini-chart to the front # with the upward arrow, then across the original stat/chart boundary. # A direct drag onto the first card also uses the actual event handlers. page.evaluate('''() => { const from = document.querySelector('[data-dashboard-widget="wide_transform_activity"] .overview-inline-grip'); const to = document.querySelector('[data-dashboard-widget="requests"] .overview-inline-grip'); const transfer = new DataTransfer(); from.dispatchEvent(new DragEvent('dragstart', {bubbles:true, cancelable:true, dataTransfer:transfer})); to.dispatchEvent(new DragEvent('drop', {bubbles:true, cancelable:true, dataTransfer:transfer, clientX:0})); from.dispatchEvent(new DragEvent('dragend', {bubbles:true})); }''') assert page.evaluate('window.rustProxyInlineLayout.currentDraft().order[0]') == 'wide_transform_activity' # Background status and unrelated preference changes cannot reset live sizes. page.evaluate('''() => { const status={revision:5,backend_pools:[],streams:{active:[],desired:[],metrics:{}}}; window.rustProxyBackendDashboard.render(status); window.rustProxyStreamDashboard.render(status); applyAdminPreferences({...window.__savedDashboard, chart_style:'soft'}); }''') assert page.locator(selector).get_attribute('data-dashboard-span') == '4' # Cancel should remove both size and layout changes. page.locator('#overview-inline-cancel').click() assert page.locator(selector).get_attribute('data-dashboard-span') == '2' assert page.locator('[data-dashboard-widget="wide_transform_activity"]').get_attribute('data-dashboard-span') == '6' assert page.evaluate('window.__savedDashboard.dashboard_widget_sizes') is None # The actual unlocked dashboard remains the only layout editor. page.locator('#overview-layout-unlock').click() page.wait_for_function('window.rustProxyInlineLayout.isEditing()') tile = '[data-dashboard-widget="wide_transform_activity"]' page.locator(tile + ' .overview-inline-narrow').click() assert page.locator(tile).get_attribute('data-dashboard-span') == '4' page.locator(tile + ' .overview-inline-narrow').click() assert page.locator(tile).get_attribute('data-dashboard-span') == '3' page.evaluate('''() => { const from = document.querySelector('[data-dashboard-widget="wide_transform_activity"] .overview-inline-grip'); const to = document.querySelector('[data-dashboard-widget="requests"] .overview-inline-grip'); const transfer = new DataTransfer(); from.dispatchEvent(new DragEvent('dragstart', {bubbles:true,cancelable:true,dataTransfer:transfer})); to.dispatchEvent(new DragEvent('drop', {bubbles:true,cancelable:true,dataTransfer:transfer,clientX:0})); from.dispatchEvent(new DragEvent('dragend', {bubbles:true})); }''') page.locator('#overview-inline-save').click() page.wait_for_function('!window.rustProxyInlineLayout.isEditing()') assert page.evaluate('window.__savedDashboard.dashboard_widget_sizes.wide_transform_activity') == 3 assert page.evaluate('window.__savedDashboard.dashboard_widgets[0]') == 'wide_transform_activity' # Reload account settings: new card width persists. page.evaluate('applyAdminPreferences(window.__savedDashboard)') assert page.locator('[data-dashboard-widget="wide_transform_activity"]').get_attribute('data-dashboard-span') == '3' assert page.evaluate('window.__savedDashboard.dashboard_density') == 'compact' # Four charts in one row, set through the same layout function used by UI. data = page.evaluate('''() => { const ids=['wide_transform_activity','wide_compression_activity','http_protocol','websocket_protocol']; const order=[...ids,...ADMIN_DASHBOARD_WIDGETS.filter(id => !ids.includes(id))]; const hidden=[...ADMIN_DASHBOARD_WIDGETS.filter(id => !ids.includes(id))]; const sizes=Object.fromEntries(ids.map(id => [id,3])); applyDashboardLayout(order,hidden,sizes); const grid=document.getElementById('dashboard-overview-grid'); return { gridWidth:grid.getBoundingClientRect().width, template:getComputedStyle(grid).gridTemplateColumns.split(' ').length, boxes:ids.map(id=>{const b=document.querySelector(`[data-dashboard-widget="${id}"]`).getBoundingClientRect();return {id,x:Math.round(b.x),y:Math.round(b.y),w:Math.round(b.width),scroll:b.width};}) }; }''') if data['template'] == 12: assert len(set(b['y'] for b in data['boxes'])) == 1, (width, data) assert all(b['w'] > 150 for b in data['boxes']), (width,data) assert page.evaluate('document.documentElement.scrollWidth - window.innerWidth') <= 2, (width, data) 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 page.evaluate('window.__savedDashboard.dashboard_widget_sizes') is None assert page.evaluate('window.__savedDashboard.dashboard_widgets') == PRESET['dashboard_widgets'] assert page.evaluate('document.getElementById("dashboard-overview-grid").dataset.overviewLayoutMode') == 'legacy' assert not errors, (width, errors) print('PASS free-order, resize, live refresh, cancel/save, four chart grid', width, data['template']) context.close() browser.close() print('PASS 0.33.3 resizable Overview browser regression')