#!/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 = (''
'
'
'
' +
''.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')