v0.33.7
This commit is contained in:
1 parent
162e986d37
commit
f462ca7194
26 files changed
+980
-710
No files matched your search
@@ -1,124 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""0.33.5 CPU plot autoscaling regression with production HTML/CSS/JS.
|
||||
|
||||
Uses Chromium with simulated authenticated DOM and status samples, not a live
|
||||
RustProxy server, SSE stream or real CPU telemetry.
|
||||
"""
|
||||
from pathlib import Path
|
||||
import re
|
||||
from playwright.sync_api import sync_playwright
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
HTML = (ROOT / 'src/admin/web/index.html').read_text(encoding='utf-8')
|
||||
CSS = (ROOT / 'src/admin/web/style.css').read_text(encoding='utf-8')
|
||||
WIDE_JS = (ROOT / 'src/admin/web/app/overview-wide-metrics.js').read_text(encoding='utf-8')
|
||||
|
||||
|
||||
def inspect(page):
|
||||
return page.evaluate('''() => {
|
||||
const get = id => document.querySelector(`#dashboard-overview-grid [data-dashboard-widget="${id}"]`);
|
||||
const cpu = get('wide_cpu'), cpuSvg = cpu.querySelector('svg');
|
||||
const protocolSvg = get('wide_protocol_rates').querySelector('svg');
|
||||
const backendSvg = get('backend_traffic').querySelector('svg');
|
||||
const legend = cpu.querySelector('.overview-wide-legend').getBoundingClientRect();
|
||||
const bounds = cpu.getBoundingClientRect();
|
||||
const matrix = cpuSvg.getScreenCTM();
|
||||
return {
|
||||
cpuHeight: cpuSvg.getBoundingClientRect().height,
|
||||
protocolHeight: protocolSvg.getBoundingClientRect().height,
|
||||
backendHeight: backendSvg.getBoundingClientRect().height,
|
||||
svgWidth: cpuSvg.getBoundingClientRect().width,
|
||||
drawnWidth: Math.hypot(matrix.a, matrix.b) * cpuSvg.viewBox.baseVal.width,
|
||||
ratio: cpuSvg.getAttribute('preserveAspectRatio'),
|
||||
cpuSeries: cpuSvg.querySelectorAll('.overview-wide-series').length,
|
||||
lineEffect: getComputedStyle(cpuSvg.querySelector('.overview-wide-series')).vectorEffect,
|
||||
gridEffect: getComputedStyle(cpuSvg.querySelector('.chart-grid')).vectorEffect,
|
||||
legendInCard: legend.top >= bounds.top && legend.bottom <= bounds.bottom + 1,
|
||||
overflow: document.documentElement.scrollWidth - innerWidth,
|
||||
};
|
||||
}''')
|
||||
|
||||
|
||||
def render_cpu(page, revision, system, process):
|
||||
return page.evaluate('''({revision,system,process}) => {
|
||||
window.rustProxyWideOverview.render({revision,live_system:{
|
||||
cpu_percent:system, process_cpu_percent:process
|
||||
}});
|
||||
const read = name => {
|
||||
const prefix = `overview-wide-wide_cpu-${name}`;
|
||||
return {
|
||||
path: document.getElementById(`${prefix}-line`).getAttribute('d'),
|
||||
label: document.getElementById(`${prefix}-value`).textContent,
|
||||
};
|
||||
};
|
||||
return {system:read('system'),process:read('process')};
|
||||
}''', {'revision': revision, 'system': system, 'process': process})
|
||||
|
||||
|
||||
def ys(path):
|
||||
return [int(v) for v in re.findall(r'[ML]\d+\s+(\d+)', path)]
|
||||
|
||||
|
||||
with sync_playwright() as p:
|
||||
browser = p.chromium.launch(headless=True, executable_path='/usr/bin/chromium', args=['--no-sandbox'])
|
||||
for viewport in (320, 375, 768, 1440, 1920):
|
||||
page = browser.new_page(viewport={'width': viewport, 'height': 950})
|
||||
errors = []
|
||||
page.on('pageerror', lambda error: errors.append(str(error)))
|
||||
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.evaluate('''() => {
|
||||
document.documentElement.classList.remove('app-loading', 'app-startup-failed');
|
||||
document.getElementById('workspace').hidden = false;
|
||||
document.getElementById('panel-overview').hidden = false;
|
||||
const grid = document.getElementById('dashboard-overview-grid');
|
||||
grid.classList.add('is-aligned');
|
||||
for (const card of grid.querySelectorAll(':scope > [data-dashboard-widget]')) {
|
||||
card.hidden = !['wide_cpu', 'wide_protocol_rates', 'backend_traffic'].includes(card.dataset.dashboardWidget);
|
||||
}
|
||||
window.currentPanel = 'overview';
|
||||
window.$ = id => document.getElementById(id);
|
||||
window.fmt = (value, digits = 0) => Number(value).toFixed(digits);
|
||||
window.fmtBytes = value => `${value} B`;
|
||||
window.rustProxyOverviewSamples = {getLimit: () => 60, onChange: () => {}};
|
||||
}''')
|
||||
page.add_script_tag(content=WIDE_JS)
|
||||
|
||||
for density in ('compact', 'normal', 'ultra'):
|
||||
page.evaluate('(density) => document.documentElement.dataset.dashboardDensity = density', density)
|
||||
result = inspect(page)
|
||||
assert result['protocolHeight'] >= 124, (viewport, density, result)
|
||||
assert abs(result['cpuHeight'] - result['protocolHeight']) <= 1, (
|
||||
'CPU must use standard chart height', viewport, density, result)
|
||||
assert result['backendHeight'] > 0, (viewport, density, result)
|
||||
assert result['ratio'] == 'none' and result['cpuSeries'] == 2, result
|
||||
assert result['lineEffect'] == result['gridEffect'] == 'non-scaling-stroke', result
|
||||
assert result['svgWidth'] > 100 and abs(result['svgWidth'] - result['drawnWidth']) < 2, result
|
||||
assert result['legendInCard'] and result['overflow'] <= 2, (viewport, density, result)
|
||||
|
||||
# Measured 0.5-0.8% must be visible, not flattened by a 100% ceiling.
|
||||
first = render_cpu(page, 1, 0.5, 0.2)
|
||||
second = render_cpu(page, 1, 0.8, 0.6)
|
||||
assert first['system']['label'] == '0.5%' and second['process']['label'] == '0.6%'
|
||||
assert ys(second['system']['path']) == [77, 41], (viewport, second)
|
||||
assert ys(second['process']['path']) == [113, 65], (viewport, second)
|
||||
|
||||
# The peak follows observed history and also works above 100% (multi-core).
|
||||
third = render_cpu(page, 1, 3.4, 2.2)
|
||||
assert ys(third['system']['path'])[-1] == 17, (viewport, third)
|
||||
assert ys(third['process']['path'])[-1] == 59, (viewport, third)
|
||||
high = render_cpu(page, 1, 250, 123)
|
||||
assert high['system']['label'] == '250.0%'
|
||||
assert ys(high['system']['path'])[-1] == 17, (viewport, high)
|
||||
assert 17 <= ys(high['process']['path'])[-1] < 137, (viewport, high)
|
||||
|
||||
# Revision changes reset the history. Zero stays at the plot baseline.
|
||||
idle = render_cpu(page, 2, 0, 0)
|
||||
assert ys(idle['system']['path']) == [137] and ys(idle['process']['path']) == [137]
|
||||
missing = render_cpu(page, 3, None, None)
|
||||
assert missing['system']['path'] == '' and missing['process']['label'] == '-'
|
||||
assert not errors, (viewport, errors)
|
||||
print(f'PASS CPU auto Y-scale and standard plot height at {viewport}px (all densities)')
|
||||
page.close()
|
||||
browser.close()
|
||||
@@ -166,13 +166,25 @@ with sync_playwright() as p:
|
||||
window.rustProxyStreamDashboard.render(status);
|
||||
}''')
|
||||
assert_visible(page, ['backend_health'], ['backend_traffic'])
|
||||
# Restore defaults in the modal after a custom layout was saved.
|
||||
# 0.33.7: the former layout modal is a preset manager and must not mutate
|
||||
# the active custom layout merely by opening/closing it.
|
||||
page.locator('#overview-layout-open').evaluate('(node) => node.hidden = false')
|
||||
page.locator('#overview-layout-open').click()
|
||||
page.wait_for_function("document.getElementById('overview-layout-dialog').open")
|
||||
page.locator('#overview-layout-default').click()
|
||||
page.locator('#overview-layout-save').click()
|
||||
assert page.locator('#overview-preset-name').is_visible()
|
||||
assert page.locator('#overview-layout-default').count() == 0
|
||||
assert_visible(page, ['backend_health'], ['backend_traffic'])
|
||||
page.locator('#overview-layout-cancel').click()
|
||||
page.wait_for_function("!document.getElementById('overview-layout-dialog').open")
|
||||
assert_visible(page, ['backend_health'], ['backend_traffic'])
|
||||
# Restore factory defaults through the same account preference API used
|
||||
# by Settings/inline editing rather than the preset manager.
|
||||
page.evaluate('''preset => window.rustProxyPreferences.update({
|
||||
dashboard_density: preset.dashboard_density,
|
||||
dashboard_widgets: [...preset.dashboard_widgets],
|
||||
dashboard_hidden_widgets: [...preset.dashboard_hidden_widgets],
|
||||
dashboard_widget_sizes: null,
|
||||
})''', PRESET)
|
||||
assert_visible(page, ['backend_traffic'], ['backend_health', 'stream_sessions', 'www_status'])
|
||||
assert page.evaluate('window.__savedDashboard.dashboard_widgets') == PRESET['dashboard_widgets']
|
||||
assert page.evaluate('window.__savedDashboard.dashboard_hidden_widgets') == PRESET['dashboard_hidden_widgets']
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Regression: Overview visual layout editor fits without horizontal overflow.
|
||||
"""Responsive regression for the Overview toolbar modal.
|
||||
|
||||
Runs the real panel DOM/CSS/bundled JavaScript with mocked preferences.
|
||||
This is browser layout testing, not Rust server acceptance.
|
||||
0.33.7 repurposed the former visual layout editor as Dashboard presets. Keep
|
||||
this older viewport regression focused on modal fit/no horizontal overflow.
|
||||
"""
|
||||
import json
|
||||
import re
|
||||
@@ -10,17 +10,17 @@ from pathlib import Path
|
||||
from playwright.sync_api import sync_playwright
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
HTML = (ROOT / 'src/admin/web/index.html').read_text()
|
||||
CSS = (ROOT / 'src/admin/web/style.css').read_text()
|
||||
ROUTER = (ROOT / 'src/admin/router.rs').read_text()
|
||||
HTML = (ROOT / "src/admin/web/index.html").read_text(encoding="utf-8")
|
||||
CSS = (ROOT / "src/admin/web/style.css").read_text(encoding="utf-8")
|
||||
ROUTER = (ROOT / "src/admin/router.rs").read_text(encoding="utf-8")
|
||||
PACKS = {
|
||||
name: {'id': name, **json.loads((ROOT / 'langs' / (name + '.json')).read_text())}
|
||||
for name in ('en_US', 'pl_pl')
|
||||
name: {"id": name, **json.loads((ROOT / "langs" / f"{name}.json").read_text(encoding="utf-8"))}
|
||||
for name in ("en_US", "pl_pl")
|
||||
}
|
||||
|
||||
|
||||
def app_bundle():
|
||||
match = re.search(r'fn admin_app_javascript\([^)]*\)[^{]*\{', ROUTER)
|
||||
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()]
|
||||
@@ -29,141 +29,64 @@ def app_bundle():
|
||||
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})();')
|
||||
result.append((ROOT / "src/admin" / include.group(1)).read_text(encoding="utf-8"))
|
||||
elif line == "admin_vhosts_javascript(),":
|
||||
result.append("(() => {\n" + "\n".join(
|
||||
(ROOT / "src/admin/web/vhosts" / f"{name}.js").read_text(encoding="utf-8")
|
||||
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)
|
||||
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-layout-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-layout-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,
|
||||
};
|
||||
}''')
|
||||
|
||||
|
||||
with sync_playwright() as p:
|
||||
browser = p.chromium.launch(headless=True, executable_path='/usr/bin/chromium', args=['--no-sandbox'])
|
||||
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})
|
||||
page = context.new_page()
|
||||
page = browser.new_page(viewport={"width": width, "height": 823})
|
||||
errors = []
|
||||
page.on('pageerror', lambda err: errors.append(str(err)))
|
||||
load(page)
|
||||
page.locator('#overview-layout-open').click()
|
||||
page.on("pageerror", lambda error: errors.append(str(error)))
|
||||
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.rustProxyPreferences && window.rustProxyDashboardLayout")
|
||||
page.evaluate("""packs => {
|
||||
languagePacks = new Map(Object.entries(packs)); defaultLanguage = 'en_US';
|
||||
language = 'pl_pl'; languagePreference = 'pl_pl';
|
||||
document.documentElement.classList.remove('app-loading', 'app-startup-failed');
|
||||
document.getElementById('workspace').hidden = false;
|
||||
document.getElementById('panel-overview').hidden = false;
|
||||
document.getElementById('overview-layout-open').hidden = false;
|
||||
currentPanel = 'overview';
|
||||
window.__savedDashboard = {...structuredClone(ADMIN_APPEARANCE_DEFAULTS), language:'pl_pl'};
|
||||
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'; languagePreference = 'pl_pl';
|
||||
for (const node of document.querySelectorAll('#overview-layout-dialog [data-i18n]'))
|
||||
node.textContent = t(node.dataset.i18n);
|
||||
}""", PACKS)
|
||||
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")
|
||||
check = page.locator("#overview-layout-dialog").evaluate("""d => {
|
||||
const r = d.getBoundingClientRect();
|
||||
const rows = [...d.querySelectorAll('.dashboard-preset-save,.dashboard-preset-row')];
|
||||
return {
|
||||
left:r.left, right:r.right, client:d.clientWidth, scroll:d.scrollWidth,
|
||||
overflow: rows.some(row => row.scrollWidth > row.clientWidth + 2),
|
||||
};
|
||||
}""")
|
||||
assert check["left"] >= -1 and check["right"] <= width + 1, (width, check)
|
||||
assert check["scroll"] <= check["client"] + 2 and not check["overflow"], (width, check)
|
||||
assert page.locator("#overview-preset-name").is_visible()
|
||||
assert page.locator("#overview-preset-save").is_visible()
|
||||
assert not errors, (width, errors)
|
||||
context.close()
|
||||
print('PASS visual modal grid, no overlap/scroll, width/hide/cancel:', width)
|
||||
page.close()
|
||||
print(f"PASS responsive Dashboard preset modal at {width}px")
|
||||
browser.close()
|
||||
print('PASS 0.33.3 modal regression (simulated API)')
|
||||
print("PASS modal responsive regression after 0.33.7 preset conversion")
|
||||
+189
@@ -0,0 +1,189 @@
|
||||
#!/usr/bin/env python3
|
||||
"""0.33.7 Dashboard preset manager and chart-title font regression.
|
||||
|
||||
Runs the bundled administration DOM/CSS/JavaScript with mocked account preferences.
|
||||
This is simulated-browser UI coverage, not a live RustProxy server test.
|
||||
"""
|
||||
import json
|
||||
import re
|
||||
from pathlib import Path
|
||||
from playwright.sync_api import sync_playwright
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
HTML = (ROOT / "src/admin/web/index.html").read_text(encoding="utf-8")
|
||||
CSS = (ROOT / "src/admin/web/style.css").read_text(encoding="utf-8")
|
||||
ROUTER = (ROOT / "src/admin/router.rs").read_text(encoding="utf-8")
|
||||
PACKS = {
|
||||
name: {"id": name, **json.loads((ROOT / "langs" / f"{name}.json").read_text(encoding="utf-8"))}
|
||||
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(encoding="utf-8"))
|
||||
elif line == "admin_vhosts_javascript(),":
|
||||
result.append("(() => {\n" + "\n".join(
|
||||
(ROOT / "src/admin/web/vhosts" / f"{name}.js").read_text(encoding="utf-8")
|
||||
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.rustProxyPreferences && window.rustProxyDashboardLayout")
|
||||
page.evaluate("""packs => {
|
||||
languagePacks = new Map(Object.entries(packs));
|
||||
defaultLanguage = 'en_US';
|
||||
language = 'pl_pl';
|
||||
document.documentElement.lang = 'pl-PL';
|
||||
document.documentElement.classList.remove('app-loading', 'app-startup-failed');
|
||||
document.getElementById('workspace').hidden = false;
|
||||
document.getElementById('panel-overview').hidden = false;
|
||||
document.getElementById('overview-layout-open').hidden = false;
|
||||
currentPanel = 'overview';
|
||||
const visible = ['connection_chart', 'traffic_chart'];
|
||||
const hidden = ADMIN_DASHBOARD_WIDGETS.filter(id => !visible.includes(id));
|
||||
window.__savedDashboard = {
|
||||
...structuredClone(ADMIN_APPEARANCE_DEFAULTS),
|
||||
language: 'pl_pl',
|
||||
dashboard_density: 'ultra',
|
||||
dashboard_widgets: [...visible, ...hidden],
|
||||
dashboard_hidden_widgets: hidden,
|
||||
dashboard_widget_sizes: {connection_chart: 6, traffic_chart: 3},
|
||||
dashboard_presets: [],
|
||||
};
|
||||
window.rustProxyPreferences.load = async () => window.__savedDashboard;
|
||||
window.rustProxyPreferences.current = () => window.__savedDashboard;
|
||||
window.rustProxyPreferences.update = async patch => {
|
||||
window.__savedDashboard = {...window.__savedDashboard, ...structuredClone(patch)};
|
||||
applyAdminPreferences(window.__savedDashboard);
|
||||
return window.__savedDashboard;
|
||||
};
|
||||
applyAdminPreferences(window.__savedDashboard);
|
||||
language = 'pl_pl'; languagePreference = 'pl_pl';
|
||||
for (const node of document.querySelectorAll('#overview-layout-dialog [data-i18n]'))
|
||||
node.textContent = t(node.dataset.i18n);
|
||||
for (const node of document.querySelectorAll('#overview-layout-dialog [data-i18n-placeholder]'))
|
||||
node.placeholder = t(node.dataset.i18nPlaceholder);
|
||||
}""", PACKS)
|
||||
|
||||
|
||||
with sync_playwright() as p:
|
||||
browser = p.chromium.launch(headless=True, executable_path="/usr/bin/chromium", args=["--no-sandbox"])
|
||||
for width in (320, 375, 768, 1440):
|
||||
page = browser.new_page(viewport={"width": width, "height": 900})
|
||||
errors = []
|
||||
page.on("pageerror", lambda error: errors.append(str(error)))
|
||||
load(page)
|
||||
|
||||
page.locator("#overview-layout-open").click()
|
||||
page.wait_for_function("document.getElementById('overview-layout-dialog').open")
|
||||
assert page.locator("#overview-layout-dialog-title").inner_text() == "Presety Dashboardu"
|
||||
assert page.locator("#overview-layout-visible").count() == 0
|
||||
assert page.locator("#overview-layout-hidden").count() == 0
|
||||
geom = page.locator("#overview-layout-dialog").evaluate("""node => ({
|
||||
left: node.getBoundingClientRect().left,
|
||||
right: node.getBoundingClientRect().right,
|
||||
client: node.clientWidth,
|
||||
scroll: node.scrollWidth,
|
||||
})""")
|
||||
assert geom["left"] >= -1 and geom["right"] <= width + 1, (width, geom)
|
||||
assert geom["scroll"] <= geom["client"] + 2, (width, geom)
|
||||
|
||||
page.locator("#overview-preset-name").fill("NOC")
|
||||
page.locator("#overview-preset-save").click()
|
||||
page.wait_for_function("window.__savedDashboard.dashboard_presets.length === 1")
|
||||
saved = page.evaluate("window.__savedDashboard.dashboard_presets[0]")
|
||||
assert saved["name"] == "NOC"
|
||||
assert saved["dashboard_density"] == "ultra"
|
||||
assert saved["dashboard_widgets"][:2] == ["connection_chart", "traffic_chart"]
|
||||
assert len(saved["dashboard_hidden_widgets"]) == len(saved["dashboard_widgets"]) - 2
|
||||
assert saved["dashboard_widget_sizes"] == {"connection_chart": 6, "traffic_chart": 3}
|
||||
assert page.locator("#overview-preset-list .dashboard-preset-row").count() == 1
|
||||
assert "2 widocznych kart" in page.locator(".dashboard-preset-info small").inner_text()
|
||||
page.locator("#overview-preset-name").fill("NOC")
|
||||
page.locator("#overview-preset-save").click()
|
||||
page.wait_for_function("document.getElementById('overview-layout-message').textContent.includes('Zaktualizowano')")
|
||||
assert page.evaluate("window.__savedDashboard.dashboard_presets.length") == 1
|
||||
|
||||
page.locator("#overview-layout-cancel").click()
|
||||
page.evaluate("""async () => {
|
||||
await window.rustProxyPreferences.update({
|
||||
dashboard_density: 'normal',
|
||||
dashboard_widgets: [...ADMIN_DASHBOARD_WIDGETS],
|
||||
dashboard_hidden_widgets: [],
|
||||
dashboard_widget_sizes: null,
|
||||
});
|
||||
}""")
|
||||
page.locator("#overview-layout-open").click()
|
||||
page.locator("#overview-preset-list .outline").click()
|
||||
page.wait_for_function("!document.getElementById('overview-layout-dialog').open")
|
||||
restored = page.evaluate("""() => ({
|
||||
density: window.__savedDashboard.dashboard_density,
|
||||
hidden: window.__savedDashboard.dashboard_hidden_widgets.length,
|
||||
sizes: window.__savedDashboard.dashboard_widget_sizes,
|
||||
})""")
|
||||
assert restored == {
|
||||
"density": "ultra",
|
||||
"hidden": len(saved["dashboard_hidden_widgets"]),
|
||||
"sizes": {"connection_chart": 6, "traffic_chart": 3},
|
||||
}, (width, restored)
|
||||
|
||||
fonts = page.evaluate("""() => {
|
||||
const family = selector => getComputedStyle(document.querySelector(selector)).fontFamily;
|
||||
return {
|
||||
connection: family('[data-dashboard-widget="connection_chart"] .small-label'),
|
||||
traffic: family('[data-dashboard-widget="traffic_chart"] .card-heading h2'),
|
||||
wide: family('[data-dashboard-widget="wide_cpu"] .card-heading h2'),
|
||||
compact: family('[data-dashboard-widget="http_protocol"] > span:first-child'),
|
||||
};
|
||||
}""")
|
||||
assert len(set(fonts.values())) == 1, (width, fonts)
|
||||
|
||||
page.locator("#overview-layout-open").click()
|
||||
page.once("dialog", lambda prompt: prompt.accept())
|
||||
page.locator("#overview-preset-list .quiet").click()
|
||||
page.wait_for_function("window.__savedDashboard.dashboard_presets.length === 0")
|
||||
assert page.locator("#overview-preset-list .dashboard-preset-row").count() == 0
|
||||
assert page.locator("#overview-preset-empty").is_visible()
|
||||
|
||||
# The browser enforces the same 20-preset limit as the preferences API.
|
||||
page.evaluate("""() => {
|
||||
const prefs = window.__savedDashboard;
|
||||
prefs.dashboard_presets = Array.from({length: 20}, (_, index) => ({
|
||||
name: `Preset ${index + 1}`,
|
||||
dashboard_density: prefs.dashboard_density,
|
||||
dashboard_widgets: [...prefs.dashboard_widgets],
|
||||
dashboard_hidden_widgets: [...prefs.dashboard_hidden_widgets],
|
||||
dashboard_widget_sizes: prefs.dashboard_widget_sizes === null ? null : {...prefs.dashboard_widget_sizes},
|
||||
}));
|
||||
applyAdminPreferences(prefs);
|
||||
window.rustProxyDashboardLayout.refresh();
|
||||
}""")
|
||||
assert page.locator("#overview-preset-list .dashboard-preset-row").count() == 20
|
||||
page.locator("#overview-preset-name").fill("Preset 21")
|
||||
page.locator("#overview-preset-save").click()
|
||||
assert page.evaluate("window.__savedDashboard.dashboard_presets.length") == 20
|
||||
assert "Osiągnięto limit presetów" in page.locator("#overview-layout-message").inner_text()
|
||||
assert not errors, (width, errors)
|
||||
page.close()
|
||||
print(f"PASS Dashboard presets + chart title font at {width}px")
|
||||
browser.close()
|
||||
print("PASS 0.33.7 Dashboard preset manager regression (simulated API)")
|
||||
@@ -142,25 +142,23 @@ with sync_playwright() as p:
|
||||
assert page.locator(selector).get_attribute('data-dashboard-span') == '2'
|
||||
assert page.locator('[data-dashboard-widget="wide_transform_activity"]').get_attribute('data-dashboard-span') == '6'
|
||||
assert page.evaluate('window.__savedDashboard.dashboard_widget_sizes') is None
|
||||
# In a fresh draft, the modal editor also provides width controls.
|
||||
page.locator('#overview-layout-open').click()
|
||||
page.wait_for_function("document.getElementById('overview-layout-dialog').open")
|
||||
tile = '#overview-layout-visible [data-preview-widget="wide_transform_activity"]'
|
||||
page.locator(tile + ' .overview-layout-narrow').click()
|
||||
assert page.locator(tile).get_attribute('data-dashboard-span') == '4'
|
||||
page.locator(tile + ' .overview-layout-narrow').click()
|
||||
assert page.locator(tile).get_attribute('data-dashboard-span') == '3'
|
||||
# Modal drag-and-drop also works across former small/wide size groups.
|
||||
# In 0.33.7 resizing/reordering remains in the inline layout editor; the
|
||||
# former modal is now dedicated to named presets.
|
||||
page.locator('#overview-layout-unlock').click()
|
||||
page.wait_for_function('window.rustProxyInlineLayout.isEditing()')
|
||||
for _ in range(2):
|
||||
page.locator('[data-dashboard-widget="wide_transform_activity"] .overview-inline-narrow').click()
|
||||
assert page.locator('[data-dashboard-widget="wide_transform_activity"]').get_attribute('data-dashboard-span') == '3'
|
||||
page.evaluate('''() => {
|
||||
const from = document.querySelector('[data-preview-widget="wide_transform_activity"]');
|
||||
const to = document.querySelector('[data-preview-widget="requests"]');
|
||||
const from = document.querySelector('[data-dashboard-widget="wide_transform_activity"] .overview-inline-grip');
|
||||
const to = document.querySelector('[data-dashboard-widget="requests"] .overview-inline-grip');
|
||||
const transfer = new DataTransfer();
|
||||
from.dispatchEvent(new DragEvent('dragstart', {bubbles:true,cancelable:true,dataTransfer:transfer}));
|
||||
to.dispatchEvent(new DragEvent('drop', {bubbles:true,cancelable:true,dataTransfer:transfer}));
|
||||
to.dispatchEvent(new DragEvent('drop', {bubbles:true,cancelable:true,dataTransfer:transfer,clientX:0}));
|
||||
from.dispatchEvent(new DragEvent('dragend', {bubbles:true}));
|
||||
}''')
|
||||
page.locator('#overview-layout-save').click()
|
||||
page.wait_for_function("!document.getElementById('overview-layout-dialog').open")
|
||||
page.locator('#overview-inline-save').click()
|
||||
page.wait_for_function('!window.rustProxyInlineLayout.isEditing()')
|
||||
assert page.evaluate('window.__savedDashboard.dashboard_widget_sizes.wide_transform_activity') == 3
|
||||
assert page.evaluate('window.__savedDashboard.dashboard_widgets[0]') == 'wide_transform_activity'
|
||||
# Reload account settings: new card width persists.
|
||||
@@ -184,11 +182,12 @@ with sync_playwright() as p:
|
||||
assert len(set(b['y'] for b in data['boxes'])) == 1, (width, data)
|
||||
assert all(b['w'] > 150 for b in data['boxes']), (width,data)
|
||||
assert page.evaluate('document.documentElement.scrollWidth - window.innerWidth') <= 2, (width, data)
|
||||
page.locator('#overview-layout-open').click()
|
||||
page.wait_for_function("document.getElementById('overview-layout-dialog').open")
|
||||
page.locator('#overview-layout-default').click()
|
||||
page.locator('#overview-layout-save').click()
|
||||
page.wait_for_function("!document.getElementById('overview-layout-dialog').open")
|
||||
page.evaluate('''preset => window.rustProxyPreferences.update({
|
||||
dashboard_density: preset.dashboard_density,
|
||||
dashboard_widgets: [...preset.dashboard_widgets],
|
||||
dashboard_hidden_widgets: [...preset.dashboard_hidden_widgets],
|
||||
dashboard_widget_sizes: null,
|
||||
})''', PRESET)
|
||||
assert page.evaluate('window.__savedDashboard.dashboard_widget_sizes') is None
|
||||
assert page.evaluate('window.__savedDashboard.dashboard_widgets') == PRESET['dashboard_widgets']
|
||||
assert page.evaluate('document.getElementById("dashboard-overview-grid").dataset.overviewLayoutMode') == 'legacy'
|
||||
|
||||
Regular → Executable
File mode changed.
Reference in new issue
Block a user