177 lines
9.5 KiB
Python
Executable File
177 lines
9.5 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Regression: named Overview presets, responsive modal and browser actions.
|
|
|
|
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 = render_admin_index(ROOT)
|
|
CSS = (ROOT / 'src/admin/web/style.css').read_text()
|
|
ROUTER = (ROOT / 'src/admin/router.rs').read_text()
|
|
PACKS = {
|
|
name: {'id': name, **json.loads((ROOT / 'langs' / (name + '.json')).read_text())}
|
|
for name in ('en_US', 'pl_pl')
|
|
}
|
|
|
|
|
|
def app_bundle():
|
|
match = re.search(r'fn admin_app_javascript\([^)]*\)[^{]*\{', ROUTER)
|
|
assert match
|
|
body = ROUTER[match.end():]
|
|
body = body[:re.search(r'\]\s*\.join\("\\n"\)', body).start()]
|
|
result = []
|
|
for line in body.splitlines():
|
|
line = line.strip()
|
|
include = re.search(r'include_str!\("([^"]+)"\)', line)
|
|
if include:
|
|
result.append((ROOT / 'src/admin' / include.group(1)).read_text())
|
|
elif line == 'admin_vhosts_javascript(),':
|
|
result.append('(() => {\n' + '\n'.join((ROOT / 'src/admin/web/vhosts' / (name + '.js')).read_text()
|
|
for name in ('core', 'protocol', 'health', 'rates', 'editor', 'render')) + '\n})();')
|
|
elif line.startswith('"') and line.endswith('",'):
|
|
result.append(json.loads(line[:-1]))
|
|
return '\n'.join(result)
|
|
|
|
|
|
BUNDLE = app_bundle()
|
|
|
|
|
|
def load(page):
|
|
page.set_content(HTML.replace('/assets/app___RUSTPROXY_ASSET_ID__.js', '')
|
|
.replace('/assets/style___RUSTPROXY_ASSET_ID__.css', ''))
|
|
page.add_style_tag(content=CSS)
|
|
page.add_script_tag(content=BUNDLE)
|
|
page.wait_for_function('window.rustProxyInlineLayout && window.rustProxyPreferences && window.rustProxyDashboardLayout')
|
|
page.evaluate('''packs => {
|
|
languagePacks = new Map(Object.entries(packs));
|
|
defaultLanguage = 'en_US';
|
|
language = 'pl_pl';
|
|
document.documentElement.lang = 'pl-PL';
|
|
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');
|
|
document.getElementById('workspace').hidden = false;
|
|
document.getElementById('panel-overview').hidden = false;
|
|
document.getElementById('overview-layout-unlock').hidden = false;
|
|
document.getElementById('overview-layout-open').hidden = false;
|
|
currentPanel = 'overview';
|
|
window.__savedDashboard = structuredClone(ADMIN_APPEARANCE_DEFAULTS);
|
|
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;
|
|
};
|
|
applyAdminPreferences(window.__savedDashboard);
|
|
language = 'pl_pl';
|
|
document.documentElement.lang = 'pl-PL';
|
|
document.querySelectorAll('#overview-presets-dialog [data-i18n]').forEach(node => node.textContent = t(node.dataset.i18n));
|
|
}''')
|
|
|
|
|
|
|
|
# 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, 800, 1440):
|
|
context = browser.new_context(viewport={'width': width, 'height': 880})
|
|
page = context.new_page()
|
|
errors = []
|
|
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-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()
|
|
browser.close()
|
|
print('PASS Overview preset modal (simulated SQLite API; no running Rust server)')
|