#!/usr/bin/env python3 """Overview row symmetry regression using the shipped CSS/JS and simulated status. Browser DOM geometry only; does not exercise the running RustProxy backend. """ from pathlib import Path import shutil from bs4 import BeautifulSoup from playwright.sync_api import sync_playwright ROOT = Path(__file__).resolve().parents[2] / 'src/admin/web' soup = BeautifulSoup((ROOT / 'index.html').read_text(), 'html.parser') widgets = list(soup.select('#dashboard-overview-grid > [data-dashboard-widget]')) assert len(widgets) == 62 html = ('' '
' '
RP
' '
' + ''.join(str(widget) for widget in widgets) + '
') SNAPSHOT = """() => { const grid = document.getElementById('dashboard-overview-grid'); const style = getComputedStyle(grid), outer = grid.getBoundingClientRect(); const group = window.rustProxyMasonry.groupFor; return { packed: grid.classList.contains('is-packed'), gap: parseFloat(style.columnGap), columns: style.gridTemplateColumns.trim().split(/\\s+/).length, grid: {left:outer.left,right:outer.right,width:outer.width}, scroll: grid.scrollWidth, client: grid.clientWidth, cards: [...grid.children].filter(node=>node.dataset.dashboardWidget && !node.hidden).map(node=>{ const r=node.getBoundingClientRect();return {id:node.dataset.dashboardWidget, group:group(node.dataset.dashboardWidget),x:r.left,y:r.top,right:r.right, bottom:r.bottom,width:r.width,height:r.height}; }) }; }""" def validate(data, zoom, width): """Peers in each row must have one top edge and one bottom edge.""" cards = data['cards']; gap = data['gap'] * zoom assert cards and not data['packed'], (width, 'absolute/masonry mode is forbidden') assert data['scroll'] <= data['client'] + 2, (width, 'horizontal overflow') seen_groups = [] rows = {} for card in cards: assert card['width'] > 40 and card['height'] > 26, (width, card) assert card['x'] >= data['grid']['left'] - 1, (width, card) assert card['right'] <= data['grid']['right'] + 1, (width, card) if not seen_groups or seen_groups[-1] != card['group']: assert card['group'] not in seen_groups, (width, 'noncontiguous size groups') seen_groups.append(card['group']) row = round(card['y'], 1) rows.setdefault(row, []).append(card) for members in rows.values(): assert len({m['group'] for m in members}) == 1, (width, 'mixed sizes in a row', members) assert max(m['bottom'] for m in members) - min(m['bottom'] for m in members) <= 1.5, (width, 'ragged bottom', members) assert max(m['height'] for m in members) - min(m['height'] for m in members) <= 1.5, (width, 'uneven heights', members) for a,b in zip(members,members[1:]): assert abs(b['x']-a['right']-gap) <= 2.0, (width,'wrong horizontal gutter',a,b,gap) line_groups = list(rows.values()) for above, below in zip(line_groups,line_groups[1:]): a = above[0]; b=below[0] assert b['y'] >= max(v['bottom'] for v in above) + gap - 2, (width,'vertical gap',a,b) return rows with sync_playwright() as playwright: browser = playwright.chromium.launch(headless=True, executable_path=shutil.which('chromium') or shutil.which('chromium-browser'), args=['--no-sandbox']) page = browser.new_page(viewport={'width':1720,'height':950}) page.set_content(html, wait_until='domcontentloaded') page.add_script_tag(content=(ROOT / 'app/overview-masonry.js').read_text()) page.evaluate("""() => window._overviewOriginal = [...document.querySelectorAll( '#dashboard-overview-grid > [data-dashboard-widget]')].map(n=>[n.dataset.dashboardWidget,n.hidden])""") data=page.evaluate(SNAPSHOT) assert data['columns']==12 assert [card['id'] for card in data['cards'][:6]] == [ 'requests','active','bandwidth','errors','cache','memory'] validate(data,1,1720) assert len({round(v['height']) for v in data['cards'][:6]}) == 1 page.locator('#dashboard-overview-grid').screenshot( path=str(Path(__file__).parents[3] / 'overview_uniform_default_preview.png')) # Screenshot regression: custom ordering previously produced 3 distinctly # sized tiles on the same horizontal band (a metric + two tall charts). ids=['backend_connection_errors','process_diagnostics','network_diagnostics','cache_diagnostics', 'connection_chart','traffic_chart','system_chart','backend_table', 'requests','active','http_protocol','websocket_protocol', 'wide_cpu','wide_memory'] page.evaluate("""ids => { const grid=document.getElementById('dashboard-overview-grid'); for(const node of grid.children) if(node.dataset.dashboardWidget) node.hidden=!ids.includes(node.dataset.dashboardWidget); grid.prepend(...ids.map(id=>grid.querySelector(`[data-dashboard-widget="${id}"]`))); window.rustProxyMasonry.refresh(); }""", ids) changed=page.evaluate(SNAPSHOT) validate(changed,1,1720) assert [card['group'] for card in changed['cards']] == sorted(card['group'] for card in changed['cards']) assert not any(card['id']=='backend_connection_errors' and card['y'] == next(v['y'] for v in changed['cards'] if v['id']=='process_diagnostics') for card in changed['cards']) # 3 uniform diagnostic panels per desktop row, 2 large charts per row. panels=[card for card in changed['cards'] if card['group']==3] assert len({round(card['y']) for card in panels[:3]}) == 1 assert len({round(card['height']) for card in panels[:3]}) == 1 wides=[card for card in changed['cards'] if card['group']==2] assert len(wides)==2 and abs(wides[0]['height']-wides[1]['height']) < 2 page.screenshot(path=str(Path(__file__).parents[3] / 'overview_uniform_preview.png'), full_page=True) # Runtime content growth automatically expands all peers in its Grid row, # without ResizeObserver, manual coordinates or animation loops. old_height=panels[0]['height'] page.evaluate("""() => { const panel=document.querySelector('[data-dashboard-widget="process_diagnostics"]'); const child=document.createElement('div');child.id='expanded';child.style.height='175px';panel.append(child); }""") updated=page.evaluate(SNAPSHOT) validate(updated,1,1720) assert next(card for card in updated['cards'] if card['id']=='process_diagnostics')['height'] > old_height + 80 page.evaluate("() => document.getElementById('expanded').remove()") validate(page.evaluate(SNAPSHOT),1,1720) radii=page.evaluate("""() => { const node=document.querySelector('[data-dashboard-widget="process_diagnostics"]'); const bar=document.createElement('div');bar.className='overview-inline-toolbar'; bar.innerHTML=''; node.append(bar);const out=[]; for(const preset of ['default','square','subtle','rounded','extra']) { document.documentElement.dataset.cornerStyle=preset; out.push([getComputedStyle(document.getElementById('logo-test')).borderTopLeftRadius, getComputedStyle(bar.querySelector('.overview-inline-up')).borderTopLeftRadius, getComputedStyle(bar.querySelector('.overview-inline-hide')).borderTopLeftRadius]); }bar.remove();return out; }""") assert len({item[0] for item in radii})==1 assert all(item[1]==item[2] for item in radii) page.evaluate("""() => { const grid=document.getElementById('dashboard-overview-grid'); for(const node of grid.children) if(node.dataset.dashboardWidget) node.hidden=false; window.rustProxyMasonry.refresh(); }""") cases=[(1720,0,1,'normal',12),(1350,180,1,'compact',12), (1200,260,1,'normal',6),(900,0,1,'compact',6), (900,190,1,'normal',6),(720,190,1,'ultra',2), (620,0,1,'compact',2),(430,0,1,'ultra',2), (1140,200,.9,'compact',6),(1140,200,1.1,'normal',6), (1140,200,1.3,'normal',6)] for width,sidebar,zoom,density,max_columns in cases: page.set_viewport_size({'width':width,'height':950}) page.evaluate("""({sidebar,zoom,density})=>{ const shell=document.getElementById('test-shell'); shell.style.width=`${(innerWidth-sidebar)/zoom}px`; shell.style.marginLeft=`${sidebar}px`;shell.style.zoom=String(zoom); document.documentElement.dataset.dashboardDensity=density; window.rustProxyMasonry.refresh(); }""", {'sidebar':sidebar,'zoom':zoom,'density':density}) result=page.evaluate(SNAPSHOT) assert len(result['cards'])==62 assert result['columns']<=max_columns, (width, result['columns'], max_columns) validate(result,zoom,width) print(f"PASS: {width}px, zoom={zoom}, groups={len(set(c['group'] for c in result['cards']))}, " f"columns={result['columns']}, gap={result['gap']}px") # Unlock mode adds editing toolbars/padding; rows must remain uniform. page.set_viewport_size({'width':900,'height':950}) page.evaluate("""() => { const grid=document.getElementById('dashboard-overview-grid'); const shell=document.getElementById('test-shell'); shell.style.width='auto';shell.style.zoom='1';shell.style.marginLeft='0'; grid.classList.add('is-unlocked'); for(const node of grid.children) if(node.dataset.dashboardWidget) { const toolbar=document.createElement('div');toolbar.className='overview-inline-toolbar'; toolbar.innerHTML=''+ 'Wykres'+ ''+ ''+ ''; node.append(toolbar); } }""") validate(page.evaluate(SNAPSHOT),1,900) page.evaluate("""() => { const grid=document.getElementById('dashboard-overview-grid'); grid.classList.remove('is-unlocked'); grid.querySelectorAll('.overview-inline-toolbar').forEach(toolbar=>toolbar.remove()); }""") page.set_viewport_size({'width':1720,'height':950}) page.evaluate("""() => { const grid=document.getElementById('dashboard-overview-grid'),shell=document.getElementById('test-shell'); shell.style.width='auto';shell.style.marginLeft='0';shell.style.zoom='1'; document.documentElement.dataset.dashboardDensity='compact'; grid.append(...window._overviewOriginal.map(([id,hidden])=>{ const node=grid.querySelector(`[data-dashboard-widget="${id}"]`); node.hidden=hidden;return node; }));window.rustProxyMasonry.refresh(); }""") final=page.evaluate(SNAPSHOT) validate(final,1,1720) assert [node['id'] for node in final['cards'][:6]]==['requests','active','bandwidth','errors','cache','memory'] browser.close() print('PASS: 62 cards in aligned rows; no floating heights, safe resize and zoom')