#!/usr/bin/env python3 """Regression: Overview visual layout editor fits without horizontal overflow. Runs the real panel DOM/CSS/bundled JavaScript with mocked preferences. This is browser layout testing, not Rust server acceptance. """ import json import re from pathlib import Path from playwright.sync_api import sync_playwright ROOT = Path(__file__).resolve().parents[2] HTML = (ROOT / 'src/admin/web/index.html').read_text() 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-layout-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-layout-dialog [data-i18n]').forEach(node => node.textContent = t(node.dataset.i18n)); }''') def geometry(page): return page.evaluate('''() => { const d = document.getElementById('overview-layout-dialog'); const grid = document.getElementById('overview-layout-visible'); const rect = node => node.getBoundingClientRect(); const tiles = Array.from(grid.querySelectorAll('.overview-layout-tile')); const overflows = []; for (const tile of tiles) { const r = rect(tile); const toolbar = tile.querySelector('.overview-layout-tile-toolbar'); if (tile.scrollWidth > tile.clientWidth + 2) overflows.push([tile.dataset.previewWidget, 'tile-scroll', tile.clientWidth, tile.scrollWidth]); for (const button of toolbar.querySelectorAll('button')) { const b = rect(button); if (b.left < r.left - 2 || b.right > r.right + 2) overflows.push([tile.dataset.previewWidget, 'button-outside', button.className, b.left, b.right, r.left, r.right]); } const buttons = Array.from(toolbar.querySelectorAll('button')); for (let i = 0; i < buttons.length; i++) { const a = rect(buttons[i]); for (let j = i + 1; j < buttons.length; j++) { const b = rect(buttons[j]); if (a.right - b.left > 1 && b.right - a.left > 1 && a.bottom - b.top > 1 && b.bottom - a.top > 1) overflows.push([tile.dataset.previewWidget, 'overlapping-buttons', buttons[i].className, buttons[j].className]); } } } const hiddenList = document.getElementById('overview-layout-hidden'); for (const row of hiddenList.querySelectorAll('.overview-layout-hidden-item')) { const r = rect(row), button = row.querySelector('button'); const b = rect(button); if (b.right > r.right + 2 || b.left < r.left - 2) overflows.push(['hidden card', 'button-outside', b.left, b.right, r.left, r.right]); } const panelPreview = grid.querySelector('[data-preview-widget="network_diagnostics"]'); const dialogRect = rect(d); return { cols: getComputedStyle(grid).gridTemplateColumns.split(' ').length, panelWidth: panelPreview ? rect(panelPreview).width : null, dialogWidth: d.clientWidth, dialogScrollWidth: d.scrollWidth, gridWidth: grid.clientWidth, gridScrollWidth: grid.scrollWidth, dialogLeft:dialogRect.left, dialogRight:dialogRect.right, visibleCount:tiles.length, overflows, firstTileWidth:rect(tiles[0]).width, }; }''') with sync_playwright() as p: browser = p.chromium.launch(headless=True, executable_path='/usr/bin/chromium', args=['--no-sandbox']) for width in (320, 375, 550, 620, 800, 900, 960, 1158, 1440, 1920): context = browser.new_context(viewport={'width': width, 'height': 823}) page = context.new_page() errors = [] page.on('pageerror', lambda err: errors.append(str(err))) load(page) page.locator('#overview-layout-open').click() page.wait_for_function("document.getElementById('overview-layout-dialog').open") check = geometry(page) expected = 1 if width <= 620 else 2 if width <= 900 else 3 assert check['cols'] == expected, (width, check) assert check['visibleCount'] > 20 assert check['panelWidth'] is not None assert abs(check['firstTileWidth'] - check['panelWidth']) < 3, (width, check) assert check['dialogLeft'] >= -1 and check['dialogRight'] <= width + 1, (width, check) assert check['dialogScrollWidth'] <= check['dialogWidth'] + 2, (width, check) assert check['gridScrollWidth'] <= check['gridWidth'] + 2, (width, check) assert not check['overflows'], (width, check['overflows'][:20]) tile = '#overview-layout-visible [data-preview-widget="http_protocol"]' before = page.locator(tile).evaluate('(el) => el.getBoundingClientRect().width') page.locator(tile + ' .overview-layout-widen').click() assert page.locator(tile).get_attribute('data-dashboard-span') == '3' after = page.locator(tile).evaluate('(el) => el.getBoundingClientRect().width') assert abs(before - after) < 2, (width, before, after) assert not geometry(page)['overflows'] page.locator(tile + ' .overview-layout-narrow').click() assert page.locator(tile).get_attribute('data-dashboard-span') == '2' if width == 1158: page.screenshot(path=str(ROOT.parent / 'modal_preview_1158.png')) if width == 375: page.screenshot(path=str(ROOT.parent / 'modal_preview_375.png')) page.locator(tile + ' .overview-layout-hide').click() assert page.locator(tile).count() == 0 assert not geometry(page)['overflows'] page.locator('#overview-layout-cancel').click() page.wait_for_function("!document.getElementById('overview-layout-dialog').open") assert not errors, (width, errors) context.close() print('PASS visual modal grid, no overlap/scroll, width/hide/cancel:', width) browser.close() print('PASS 0.33.3 modal regression (simulated API)')