207 lines
12 KiB
Python
Executable File
207 lines
12 KiB
Python
Executable File
#!/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')
|