Files
RustProxy/tools/checks/overview_grid_spacing_0323_browser.py
T
2026-10-13 21:29:57 +02:00

207 lines
12 KiB
Python
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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 = ('<!doctype html><html data-theme="ultra_dark" data-dashboard-density="compact">'
'<head><meta charset="utf-8"><style>' + (ROOT / 'style.css').read_text() +
'</style></head><body style="margin:0"><div id="test-shell" '
'style="padding:8px;box-sizing:border-box"><div class="brand">'
'<div class="brand-mark" id="logo-test">R<span>P</span></div></div>'
'<section id="panel-overview"><div id="dashboard-overview-grid" '
'class="stats-grid dashboard-overview-grid">' +
''.join(str(widget) for widget in widgets) + '</div></section></div></body></html>')
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='<button class="quiet overview-inline-up">←</button><button class="quiet overview-inline-hide">×</button>';
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',1),
(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='<button class="quiet overview-inline-grip">⠿</button>'+
'<span class="overview-widget-type">Wykres</span>'+
'<button class="quiet overview-inline-up">←</button>'+
'<button class="quiet overview-inline-down">→</button>'+
'<button class="quiet overview-inline-hide">×</button>';
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')