116 lines
6.8 KiB
Python
Executable File
116 lines
6.8 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Verify sidebar colors use only the UI theme; chart palettes affect charts only.
|
|
|
|
Uses actual production HTML/CSS in headless Chromium, not a live RustProxy
|
|
server or authenticated preference/session roundtrip.
|
|
"""
|
|
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')
|
|
THEMES = ('dark', 'ultra_dark', 'light', 'cream')
|
|
PALETTES = ('default', 'soft', 'technical', 'high_contrast', 'neon', 'monochrome', 'warm', 'cool')
|
|
CORNERS = {'default': 0, 'square': 0, 'subtle': 4, 'rounded': 8, 'extra': 12}
|
|
|
|
STYLE_INFO = r'''() => {
|
|
const color = value => {
|
|
const c = document.createElement('canvas'); c.width = c.height = 1;
|
|
const ctx = c.getContext('2d'); ctx.fillStyle = value; ctx.fillRect(0, 0, 1, 1);
|
|
return [...ctx.getImageData(0, 0, 1, 1).data].slice(0, 3);
|
|
};
|
|
const root = getComputedStyle(document.documentElement);
|
|
const active = document.querySelector('.sidebar .main-navigation .nav.active');
|
|
const idle = document.querySelector('.sidebar .main-navigation .nav:not(.active)');
|
|
const sa = getComputedStyle(active), si = getComputedStyle(idle);
|
|
return {
|
|
chartAccent:color(root.getPropertyValue('--chart-accent').trim()),
|
|
themeSelected:color(root.getPropertyValue('--nav-active-bg').trim()),
|
|
themeHovered:color(root.getPropertyValue('--nav-hover-bg').trim()),
|
|
foreground:color(sa.color),
|
|
background:color(sa.backgroundColor),
|
|
borderLeft:color(sa.borderLeftColor),
|
|
borderRight:color(sa.borderRightColor),
|
|
radius:parseFloat(sa.borderTopLeftRadius),
|
|
idleBackground:color(si.backgroundColor),
|
|
mobileBackground:color(getComputedStyle(document.querySelector('.mobile-menu-toggle')).backgroundColor),
|
|
mobileColor:color(getComputedStyle(document.querySelector('.mobile-menu-toggle')).color),
|
|
navWidth:document.querySelector('.main-navigation').scrollWidth,
|
|
navClientWidth:document.querySelector('.main-navigation').clientWidth,
|
|
activeWidth:active.scrollWidth,
|
|
activeClientWidth:active.clientWidth,
|
|
};
|
|
}'''
|
|
|
|
def contrast(fg, bg):
|
|
def luminance(rgb):
|
|
channels = [x / 255 for x in rgb]
|
|
channels = [x / 12.92 if x <= 0.04045 else ((x + 0.055) / 1.055) ** 2.4 for x in channels]
|
|
return .2126 * channels[0] + .7152 * channels[1] + .0722 * channels[2]
|
|
lo, hi = sorted((luminance(fg), luminance(bg)))
|
|
return (hi + .05) / (lo + .05)
|
|
|
|
with sync_playwright() as playwright:
|
|
browser = playwright.chromium.launch(headless=True, executable_path='/usr/bin/chromium', args=['--no-sandbox'])
|
|
for viewport in (375, 1200):
|
|
page = browser.new_page(viewport={'width': viewport, 'height': 820})
|
|
errors = []
|
|
page.on('pageerror', lambda error: errors.append(str(error)))
|
|
page.set_content(HTML.replace('/assets/style___RUSTPROXY_ASSET_ID__.css', '').replace('/assets/app___RUSTPROXY_ASSET_ID__.js', ''))
|
|
page.add_style_tag(content=CSS)
|
|
page.evaluate('''() => {
|
|
document.documentElement.classList.remove('app-loading');
|
|
document.getElementById('app-startup').remove();
|
|
document.querySelector('.sidebar').classList.add('mobile-nav-open');
|
|
document.querySelector('.mobile-menu-toggle')?.setAttribute('aria-expanded', 'true');
|
|
}''')
|
|
theme_colors = []
|
|
for theme in THEMES:
|
|
baseline = None
|
|
chart_accents = set()
|
|
for palette in PALETTES:
|
|
page.evaluate('''([theme, palette]) => {
|
|
document.documentElement.dataset.theme = theme;
|
|
document.documentElement.dataset.chartStyle = palette;
|
|
}''', [theme, palette])
|
|
for corner, expected_radius in CORNERS.items():
|
|
page.evaluate('corner => document.documentElement.dataset.cornerStyle = corner', corner)
|
|
style = page.evaluate(STYLE_INFO)
|
|
assert style['radius'] == expected_radius, (viewport, theme, palette, corner, style['radius'])
|
|
assert style['borderLeft'] == style['foreground'], (viewport, theme, palette, corner, style)
|
|
assert style['background'] == style['themeSelected'], (viewport, theme, palette, corner, style)
|
|
if viewport == 375:
|
|
assert style['mobileBackground'] == style['background'], (viewport, theme, palette, corner, style)
|
|
assert style['mobileColor'] == style['foreground'], (viewport, theme, palette, corner, style)
|
|
assert contrast(style['foreground'], style['background']) >= 4.5, (viewport, theme, palette, corner, style)
|
|
assert style['navWidth'] <= style['navClientWidth'] + 2, (viewport, theme, palette, corner, style)
|
|
assert style['activeWidth'] <= style['activeClientWidth'] + 2, (viewport, theme, palette, corner, style)
|
|
chart_accents.add(tuple(style['chartAccent']))
|
|
# The entire selected state is unaffected by chart-style changes.
|
|
nav_colors = (tuple(style['foreground']), tuple(style['background']), tuple(style['borderLeft']))
|
|
if baseline is None:
|
|
baseline = nav_colors
|
|
else:
|
|
assert baseline == nav_colors, (viewport, theme, palette, baseline, nav_colors)
|
|
page.locator('.nav[data-panel="vhosts"]').hover()
|
|
hovered = page.evaluate(STYLE_INFO)
|
|
assert hovered['idleBackground'] == hovered['themeHovered'], (viewport, theme, palette, hovered)
|
|
assert hovered['idleBackground'] != style['idleBackground'], (viewport, theme, palette)
|
|
page.mouse.move(0, 0)
|
|
if viewport == 1200 and theme == 'ultra_dark' and palette == 'cool':
|
|
page.screenshot(path=str(ROOT.parent / 'sidebar_preview_desktop.png'))
|
|
if viewport == 375 and theme == 'dark' and palette == 'soft':
|
|
page.screenshot(path=str(ROOT.parent / 'sidebar_preview_mobile.png'))
|
|
assert len(chart_accents) > 1, (viewport, theme, chart_accents)
|
|
theme_colors.append(baseline)
|
|
assert len(set(theme_colors)) == len(THEMES), (viewport, theme_colors)
|
|
page.evaluate("document.documentElement.dir = 'rtl'")
|
|
rtl = page.evaluate(STYLE_INFO)
|
|
assert rtl['borderRight'] == rtl['foreground'], (viewport, rtl)
|
|
assert not errors, errors
|
|
print('PASS themed navigation independence: viewport', viewport, 'themes', len(THEMES), 'palettes', len(PALETTES), 'corners', len(CORNERS))
|
|
page.close()
|
|
browser.close()
|
|
print('PASS: sidebar changes with UI theme and corner setting, never with chart style')
|