Files
RustProxy/tools/checks/appearance_ux_0323_browser.py
T

88 lines
6.3 KiB
Python
Executable File

#!/usr/bin/env python3
"""Optional Chromium checks for Appearance controls and global corner geometry.
Uses shipped CSS and the real Appearance list-rendering function with mocked
preferences/translation state. Not a test of the authenticated Rust service.
"""
from pathlib import Path
import json
import shutil
from playwright.sync_api import sync_playwright
ROOT = Path(__file__).resolve().parents[2]
WEB = ROOT / 'src/admin/web'
css = (WEB / 'style.css').read_text()
prefs = (WEB / 'app/preferences.js').read_text()
widgets = json.loads((WEB / 'dashboard-widgets.json').read_text())
translations = json.loads((ROOT / 'langs/en_US.json').read_text())['translations']
functions = prefs[:prefs.index('function browserLanguage()')] + '\n' + prefs[prefs.index('function dashboardWidgetLabel'):prefs.index('function refreshChartStylePalette')]
html = (f'<!doctype html><html data-theme="ultra_dark" data-corner-style="default"><head><style>{css}</style></head><body>'
'<div id="settings-page-appearance" class="settings-page" style="max-width:100%;padding:12px">'
'<div class="appearance-section"><div class="dashboard-widget-list" id="dashboard-widget-list"></div></div>'
'<div style="display:flex;gap:10px;margin-top:12px">'
'<div class="card" id="test-card">Card</div><div class="stat" id="test-stat">Stat</div>'
'<button class="outline" id="test-action">Action</button><input id="test-input" value="value"></div>'
'<a class="brand" href="#"><span class="brand-mark" id="test-logo">r<span>/</span>p</span>RustProxy</a></div>'
'<div class="overview-widget-type" id="editor-type-badge">Chart</div></body></html>')
js = f'''const ADMIN_DASHBOARD_WIDGET_REGISTRY = Object.freeze({json.dumps(widgets)});
const translations = {json.dumps(translations)};
function t(key) {{ return translations[key] || key; }}
function $(id) {{ return document.getElementById(id); }}
function el(tag, text, klass) {{ const node = document.createElement(tag); if (text != null) node.textContent = text; if (klass) node.className = klass; return node; }}
{functions}
renderDashboardWidgetSettings();'''
with sync_playwright() as playwright:
binary = shutil.which('chromium') or shutil.which('chromium-browser')
browser = playwright.chromium.launch(headless=True, executable_path=binary, args=['--no-sandbox'])
page = browser.new_page(viewport={'width': 1320, 'height': 950})
page.set_content(html, wait_until='domcontentloaded')
page.add_script_tag(content=js)
assert page.locator('#dashboard-widget-list .dashboard-widget-setting').count() == 62
assert page.locator('#settings-page-appearance .overview-widget-type').count() == 0
assert page.locator('#editor-type-badge').is_visible()
for width in (1320, 790, 450, 345):
page.set_viewport_size({'width': width, 'height': 950})
results = page.locator('.dashboard-widget-setting').evaluate_all('''rows => rows.map(row => {
const label = row.querySelector('.dashboard-widget-toggle').getBoundingClientRect();
const actions = row.querySelector('.dashboard-widget-setting-actions').getBoundingClientRect();
const box = row.getBoundingClientRect();
return { labelRight: label.right, actionsLeft: actions.left, actionsRight: actions.right, right: box.right };
})''')
for i, result in enumerate(results):
assert result['actionsLeft'] >= result['labelRight'] - 1, (width, i, result)
assert result['actionsRight'] <= result['right'] + 1, (width, i, result)
assert page.evaluate('document.documentElement.scrollWidth <= innerWidth + 2'), width
for theme in ('dark', 'light', 'ultra_dark', 'cream'):
page.evaluate('theme => { document.documentElement.dataset.theme = theme; document.documentElement.dataset.cornerStyle = "default"; }', theme)
stat = page.locator('#test-stat').evaluate('(n) => getComputedStyle(n).borderTopLeftRadius')
card = page.locator('#test-card').evaluate('(n) => getComputedStyle(n).borderTopLeftRadius')
assert stat == card == '0px', (theme, stat, card)
for style, expected_surface, expected_control in (('square', '0px', '0px'), ('subtle','5px','4px'), ('rounded','12px','8px'),('extra','18px','12px')):
page.evaluate('style => document.documentElement.dataset.cornerStyle = style', style)
assert page.locator('#test-stat').evaluate('(n) => getComputedStyle(n).borderTopLeftRadius') == expected_surface, (theme, style, 'stat')
assert page.locator('#test-card').evaluate('(n) => getComputedStyle(n).borderTopLeftRadius') == expected_surface, (theme, style, 'card')
assert page.locator('#test-action').evaluate('(n) => getComputedStyle(n).borderTopLeftRadius') == expected_control, (theme, style, 'button')
assert page.locator('#test-input').evaluate('(n) => getComputedStyle(n).borderTopLeftRadius') == expected_control, (theme, style, 'input')
page.evaluate('document.documentElement.dataset.theme = "dark"; document.documentElement.dataset.cornerStyle = "default"')
# The last glyph must have room before the outlined logo's right edge,
# including the 34px mobile variant; the slash-to-p spacing is unchanged.
for width in (1320, 790, 450, 345):
page.set_viewport_size({'width': width, 'height': 950})
inset = page.locator('#test-logo').evaluate('''mark => {
const p = [...mark.childNodes].find(node => node.nodeType === 3 && node.textContent.trim() === 'p');
const range = document.createRange();
range.selectNodeContents(p);
return mark.getBoundingClientRect().right - range.getBoundingClientRect().right;
}''')
assert inset >= 4, (width, inset)
page.set_viewport_size({'width': 1320, 'height': 950})
original_logo = page.locator('#test-logo').evaluate('(n) => getComputedStyle(n).borderTopColor')
page.evaluate('document.documentElement.dataset.theme = "ultra_dark"')
black_logo = page.locator('#test-logo').evaluate('(n) => getComputedStyle(n).borderTopColor')
assert black_logo == original_logo == 'rgb(246, 155, 116)', (black_logo, original_logo)
out = ROOT.parent / 'appearance_ux_0323_preview.png'
page.screenshot(path=str(out))
browser.close()
print('PASS: 62 badge-free Appearance rows, right-pinned buttons at 4 widths, theme-independent corner geometry, original black-theme logo accent')