v0.33.7
This commit is contained in:
1 parent
162e986d37
commit
6b937aaf85
54 files changed
+3527
-1325
No files matched your search
@@ -1,16 +1,17 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Regression: Overview visual layout editor fits without horizontal overflow.
|
||||
"""Regression: named Overview presets, responsive modal and browser actions.
|
||||
|
||||
Runs the real panel DOM/CSS/bundled JavaScript with mocked preferences.
|
||||
Runs the real panel DOM/CSS/bundled JavaScript with mocked preferences/API.
|
||||
This is browser layout testing, not Rust server acceptance.
|
||||
"""
|
||||
import json
|
||||
import re
|
||||
from pathlib import Path
|
||||
from panel_html import render_admin_index
|
||||
from playwright.sync_api import sync_playwright
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
HTML = (ROOT / 'src/admin/web/index.html').read_text()
|
||||
HTML = render_admin_index(ROOT)
|
||||
CSS = (ROOT / 'src/admin/web/style.css').read_text()
|
||||
ROUTER = (ROOT / 'src/admin/router.rs').read_text()
|
||||
PACKS = {
|
||||
@@ -52,7 +53,7 @@ def load(page):
|
||||
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));
|
||||
document.querySelectorAll('#overview-presets-dialog [data-i18n]').forEach(node => node.textContent = t(node.dataset.i18n));
|
||||
}''', PACKS)
|
||||
page.evaluate('''() => {
|
||||
document.documentElement.classList.remove('app-loading', 'app-startup-failed');
|
||||
@@ -72,98 +73,104 @@ def load(page):
|
||||
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));
|
||||
document.querySelectorAll('#overview-presets-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,
|
||||
};
|
||||
}''')
|
||||
|
||||
|
||||
# Regression for the replacement named-presets modal, with a simulated SQLite API.
|
||||
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})
|
||||
for width in (320, 375, 800, 1440):
|
||||
context = browser.new_context(viewport={'width': width, 'height': 880})
|
||||
page = context.new_page()
|
||||
errors = []
|
||||
page.on('pageerror', lambda err: errors.append(str(err)))
|
||||
page.on('pageerror', lambda error: errors.append(str(error)))
|
||||
load(page)
|
||||
page.evaluate('''() => {
|
||||
const saved = structuredClone(ADMIN_APPEARANCE_DEFAULTS);
|
||||
window.__savedDashboard = saved;
|
||||
window.__presets = new Map();
|
||||
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;
|
||||
};
|
||||
api = async (path, options={}) => {
|
||||
const method = options.method || 'GET';
|
||||
if (path === '/api/dashboard-presets' && method === 'GET')
|
||||
return [...window.__presets.entries()].map(([name, data]) => ({ name, ...structuredClone(data) }));
|
||||
if (path.startsWith('/api/dashboard-presets/')) {
|
||||
const name = decodeURIComponent(path.split('/').slice(3).join('/'));
|
||||
if (method === 'PUT') { window.__presets.set(name, JSON.parse(options.body)); return {}; }
|
||||
if (method === 'DELETE') { window.__presets.delete(name); return {}; }
|
||||
if (method === 'PATCH') {
|
||||
const changed = JSON.parse(options.body).name;
|
||||
if (window.__presets.has(changed)) throw new Error('already exists');
|
||||
window.__presets.set(changed, window.__presets.get(name));
|
||||
window.__presets.delete(name);
|
||||
return {};
|
||||
}
|
||||
}
|
||||
throw new Error('unexpected API: ' + method + ' ' + path);
|
||||
};
|
||||
appConfirm = async () => true;
|
||||
applyAdminPreferences(window.__savedDashboard);
|
||||
document.getElementById('overview-layout-open').hidden = false;
|
||||
}''')
|
||||
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")
|
||||
page.wait_for_function("document.getElementById('overview-presets-dialog').open")
|
||||
assert page.locator('#overview-presets-list .overview-presets-row').count() == 0
|
||||
widths = page.evaluate('''() => {
|
||||
const d = document.getElementById('overview-presets-dialog');
|
||||
const r = d.getBoundingClientRect();
|
||||
return {left:r.left,right:r.right,overflow:d.scrollWidth - d.clientWidth};
|
||||
}''')
|
||||
assert widths['left'] >= -1 and widths['right'] <= width+1 and widths['overflow'] <= 2, (width, widths)
|
||||
# Persist the factory arrangement as a user preset.
|
||||
page.locator('#overview-presets-name').fill('Daily')
|
||||
page.locator('#overview-presets-save').click()
|
||||
page.wait_for_function("window.__presets.has('Daily')")
|
||||
assert page.locator('#overview-presets-list .overview-presets-row').count() == 1
|
||||
assert page.evaluate("window.__presets.get('Daily').overview_samples") == 600
|
||||
# Create another view, with customized widths, density and history.
|
||||
page.evaluate('''async () => {
|
||||
await window.rustProxyPreferences.update({
|
||||
dashboard_hidden_widgets:['requests'], dashboard_widget_sizes:{requests:3},
|
||||
dashboard_density:'ultra',
|
||||
});
|
||||
window.rustProxyOverviewSamples.setLimit(120, true);
|
||||
}''')
|
||||
page.locator('#overview-presets-name').fill('Detailed')
|
||||
page.locator('#overview-presets-save').click()
|
||||
page.wait_for_function("window.__presets.has('Detailed')")
|
||||
assert page.evaluate('''() => {
|
||||
const p=window.__presets.get('Detailed');
|
||||
return [p.dashboard_widget_sizes.requests,p.dashboard_density,p.overview_samples];
|
||||
}''') == [3,'ultra',120]
|
||||
# Named load must restore full layout and history.
|
||||
page.locator('.overview-presets-row').filter(has_text='Daily').locator('[data-preset-action="dashboardPresetsLoad"]').click()
|
||||
page.wait_for_function("!document.getElementById('overview-presets-dialog').open")
|
||||
assert page.evaluate('''() => [window.__savedDashboard.dashboard_density,
|
||||
window.__savedDashboard.dashboard_widget_sizes,
|
||||
window.rustProxyOverviewSamples.getLimit()]''') == ['compact',None,600]
|
||||
# Rename and delete remain administrator-scoped operations.
|
||||
page.locator('#overview-layout-open').click()
|
||||
page.wait_for_function("document.getElementById('overview-presets-dialog').open")
|
||||
page.locator('#overview-presets-name').fill('Nightly')
|
||||
page.locator('.overview-presets-row').filter(has_text='Detailed').locator('[data-preset-action="dashboardPresetsRename"]').click()
|
||||
page.wait_for_function("window.__presets.has('Nightly')")
|
||||
assert not page.evaluate("window.__presets.has('Detailed')")
|
||||
page.locator('.overview-presets-row').filter(has_text='Nightly').locator('[data-preset-action="dashboardPresetsDelete"]').click()
|
||||
page.wait_for_function("!window.__presets.has('Nightly')")
|
||||
assert page.evaluate("window.__presets.has('Daily')")
|
||||
page.locator('#overview-presets-factory').click()
|
||||
page.wait_for_function("!document.getElementById('overview-presets-dialog').open")
|
||||
assert page.evaluate("window.__savedDashboard.dashboard_widgets") == json.loads((ROOT / 'src/admin/web/dashboard-defaults.json').read_text())['dashboard_widgets']
|
||||
assert not errors, (width, errors)
|
||||
print('PASS named Dashboard presets save/load/rename/delete/factory and responsive modal:', width)
|
||||
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)')
|
||||
print('PASS Overview preset modal (simulated SQLite API; no running Rust server)')
|
||||
Reference in new issue
Block a user