#!/usr/bin/env python3 """Regression: named Overview presets, responsive modal and browser actions. Runs the real panel DOM/CSS/bundled JavaScript with mocked preferences/API. This is browser layout testing, not Rust server acceptance. """ 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] HTML = render_admin_index(ROOT) CSS = (ROOT / 'src/admin/web/style.css').read_text() ROUTER = (ROOT / 'src/admin/router.rs').read_text() PACKS = { name: {'id': name, **json.loads((ROOT / 'langs' / (name + '.json')).read_text())} for name in ('en_US', 'pl_pl') } 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) BUNDLE = app_bundle() 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.rustProxyDashboardLayout') page.evaluate('''packs => { languagePacks = new Map(Object.entries(packs)); defaultLanguage = 'en_US'; language = 'pl_pl'; document.documentElement.lang = 'pl-PL'; document.querySelectorAll('#overview-presets-dialog [data-i18n]').forEach(node => node.textContent = t(node.dataset.i18n)); }''', PACKS) 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); language = 'pl_pl'; document.documentElement.lang = 'pl-PL'; document.querySelectorAll('#overview-presets-dialog [data-i18n]').forEach(node => node.textContent = t(node.dataset.i18n)); }''') # Regression for the replacement named-presets modal, with a simulated SQLite API. 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': 880}) page = context.new_page() errors = [] page.on('pageerror', lambda error: errors.append(str(error))) load(page) page.evaluate('''() => { const saved = structuredClone(ADMIN_APPEARANCE_DEFAULTS); window.__savedDashboard = saved; window.__presets = new Map(); 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; }; api = async (path, options={}) => { const method = options.method || 'GET'; if (path === '/api/dashboard-presets' && method === 'GET') return [...window.__presets.entries()].map(([name, data]) => ({ name, ...structuredClone(data) })); if (path.startsWith('/api/dashboard-presets/')) { const name = decodeURIComponent(path.split('/').slice(3).join('/')); if (method === 'PUT') { window.__presets.set(name, JSON.parse(options.body)); return {}; } if (method === 'DELETE') { window.__presets.delete(name); return {}; } if (method === 'PATCH') { const changed = JSON.parse(options.body).name; if (window.__presets.has(changed)) throw new Error('already exists'); window.__presets.set(changed, window.__presets.get(name)); window.__presets.delete(name); return {}; } } throw new Error('unexpected API: ' + method + ' ' + path); }; appConfirm = async () => true; applyAdminPreferences(window.__savedDashboard); document.getElementById('overview-layout-open').hidden = false; }''') page.locator('#overview-layout-open').click() page.wait_for_function("document.getElementById('overview-presets-dialog').open") assert page.locator('#overview-presets-list .overview-presets-row').count() == 0 widths = page.evaluate('''() => { const d = document.getElementById('overview-presets-dialog'); const r = d.getBoundingClientRect(); return {left:r.left,right:r.right,overflow:d.scrollWidth - d.clientWidth}; }''') assert widths['left'] >= -1 and widths['right'] <= width+1 and widths['overflow'] <= 2, (width, widths) # Persist the factory arrangement as a user preset. page.locator('#overview-presets-name').fill('Daily') page.locator('#overview-presets-save').click() page.wait_for_function("window.__presets.has('Daily')") assert page.locator('#overview-presets-list .overview-presets-row').count() == 1 assert page.evaluate("window.__presets.get('Daily').overview_samples") == 600 # Create another view, with customized widths, density and history. page.evaluate('''async () => { await window.rustProxyPreferences.update({ dashboard_hidden_widgets:['requests'], dashboard_widget_sizes:{requests:3}, dashboard_density:'ultra', }); window.rustProxyOverviewSamples.setLimit(120, true); }''') page.locator('#overview-presets-name').fill('Detailed') page.locator('#overview-presets-save').click() page.wait_for_function("window.__presets.has('Detailed')") assert page.evaluate('''() => { const p=window.__presets.get('Detailed'); return [p.dashboard_widget_sizes.requests,p.dashboard_density,p.overview_samples]; }''') == [3,'ultra',120] # Named load must restore full layout and history. page.locator('.overview-presets-row').filter(has_text='Daily').locator('[data-preset-action="dashboardPresetsLoad"]').click() page.wait_for_function("!document.getElementById('overview-presets-dialog').open") assert page.evaluate('''() => [window.__savedDashboard.dashboard_density, window.__savedDashboard.dashboard_widget_sizes, window.rustProxyOverviewSamples.getLimit()]''') == ['compact',None,600] # Rename and delete remain administrator-scoped operations. page.locator('#overview-layout-open').click() page.wait_for_function("document.getElementById('overview-presets-dialog').open") page.locator('#overview-presets-name').fill('Nightly') page.locator('.overview-presets-row').filter(has_text='Detailed').locator('[data-preset-action="dashboardPresetsRename"]').click() page.wait_for_function("window.__presets.has('Nightly')") assert not page.evaluate("window.__presets.has('Detailed')") page.locator('.overview-presets-row').filter(has_text='Nightly').locator('[data-preset-action="dashboardPresetsDelete"]').click() page.wait_for_function("!window.__presets.has('Nightly')") assert page.evaluate("window.__presets.has('Daily')") page.locator('#overview-presets-factory').click() page.wait_for_function("!document.getElementById('overview-presets-dialog').open") assert page.evaluate("window.__savedDashboard.dashboard_widgets") == json.loads((ROOT / 'src/admin/web/dashboard-defaults.json').read_text())['dashboard_widgets'] assert not errors, (width, errors) print('PASS named Dashboard presets save/load/rename/delete/factory and responsive modal:', width) context.close() browser.close() print('PASS Overview preset modal (simulated SQLite API; no running Rust server)')