v0.32.6_official

This commit is contained in:
Mateusz Gruszczyński committed 2026-10-14 13:04:06 +02:00
1 parent fc8711d4d8
commit d2c6cf59a9
16 files changed
+1801 -1027

No files matched your search

@@ -65,6 +65,18 @@ with sync_playwright() as playwright:
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')
+91
View File
@@ -0,0 +1,91 @@
#!/usr/bin/env python3
"""Optional real-CSS/JS Chromium regression for the horizontal Overview card ribbon.
Mocks authenticated preferences; no server or physical iOS device involved.
Run with: python tools/checks/dashboard_appearance_ribbon_0326_browser.py
"""
from pathlib import Path
from playwright.sync_api import sync_playwright
import shutil
import json
root=Path(__file__).resolve().parents[2]
html=(root/'src/admin/web/index.html').read_text()
css=(root/'src/admin/web/style.css').read_text()
prefs=(root/'src/admin/web/app/preferences.js').read_text()
registry=json.loads((root/'src/admin/web/dashboard-widgets.json').read_text())
translations=json.loads((root/'langs/pl_pl.json').read_text())['translations']
heading=html.index('data-i18n="dashboardLayout"')
section_start=html.rfind('<section class="appearance-section">',0,heading)
section_end=html.index('</section>',heading)+len('</section>')
assert section_start >= 0
source=html[section_start:section_end]
start=prefs.index('function renderDashboardWidgetSettings() {')
end=prefs.index('function refreshChartStylePalette(',start)
code=prefs[start:end]
stub='''
const $ = id => document.getElementById(id);
const el = (tag, txt, classes) => { let node=document.createElement(tag); if (txt!==undefined) node.textContent=String(txt); if (classes) node.className=classes; return node; };
const labels = __TRANSLATIONS__;
const registry = __REGISTRY__;
const ADMIN_DASHBOARD_BY_ID = new Map(registry.map(def=>[def.id,def]));
const order = registry.map(def=>def.id);
let adminPreferences = {dashboard_widgets:order, dashboard_hidden_widgets:registry.filter(def=>def.default_hidden).map(def=>def.id)};
const ADMIN_APPEARANCE_DEFAULTS = adminPreferences;
function t(key){ return labels[key] || key; }
function dashboardWidgetLabel(id) { return t(ADMIN_DASHBOARD_BY_ID.get(id)?.label_key || id); }
function normalizeDashboardWidgets(value){return Array.isArray(value)?value:order;}
function normalizeHiddenDashboardWidgets(value){return Array.isArray(value)?value:[];}
function applyDashboardLayout(order,hidden){ window.__state={order,hidden}; }
async function saveAppearancePreference(next){ Object.assign(adminPreferences,next);return {}; }
'''.replace('__TRANSLATIONS__',json.dumps(translations,ensure_ascii=False)).replace('__REGISTRY__',json.dumps(registry))
with sync_playwright() as p:
browser=p.chromium.launch(executable_path=shutil.which('chromium') or shutil.which('chromium-browser'),args=['--no-sandbox'],headless=True)
for width,height in [(1500,950),(393,850)]:
page=browser.new_page(viewport={'width':width,'height':height},device_scale_factor=1)
page.set_content('<!doctype html><html data-theme="dark" lang="pl"><head><style>'+css+'</style></head><body><main style="margin:0; padding:28px 16px; width:100%; max-width:100%; box-sizing:border-box">'+source+'</main></body></html>',wait_until='domcontentloaded')
page.add_script_tag(content=stub+code+'\nrenderDashboardWidgetSettings();')
page.wait_for_timeout(90)
metrics=page.evaluate('''() => {
const list=document.getElementById('dashboard-widget-list'), section=document.querySelector('.appearance-section');
const parent=list.closest('.dashboard-widget-settings');
return { total: list.children.length, listHeight:list.getBoundingClientRect().height,
listWidth:list.getBoundingClientRect().width, contentWidth:list.scrollWidth,
sectionHeight:section.getBoundingClientRect().height,
overflowX:getComputedStyle(list).overflowX,
firstWidth:list.firstElementChild.getBoundingClientRect().width,
density: document.querySelector('.dashboard-density-setting').getBoundingClientRect().width,
widget:parent.getBoundingClientRect().width,
viewportOverflow:document.documentElement.scrollWidth>window.innerWidth,
nextDisabled:document.getElementById('dashboard-widget-scroll-next').disabled};
}''')
print(width,'COMPACT',metrics)
assert metrics['total']==len(registry)
assert metrics['listHeight']<66,(width,metrics)
assert metrics['contentWidth']>metrics['listWidth'],(width,metrics)
assert metrics['sectionHeight'] < (320 if width>700 else 450), (width,metrics)
assert not metrics['viewportOverflow']
page.locator('#dashboard-widget-scroll-next').click()
page.wait_for_timeout(450)
pos=page.evaluate('document.getElementById("dashboard-widget-list").scrollLeft')
print(width,'scroll after next:',pos)
assert pos>10
page.locator('.dashboard-widget-setting').nth(7).locator('button.dashboard-widget-toggle').click(force=True)
page.wait_for_timeout(60)
state=page.evaluate('window.__state')
assert state and len(state['order'])==len(registry)
retained=page.evaluate('document.getElementById("dashboard-widget-list").scrollLeft')
assert retained>=pos-15,(pos,retained)
page.locator('#dashboard-widget-expand').click()
page.wait_for_timeout(60)
expanded=page.evaluate('''() => { const list = document.getElementById("dashboard-widget-list"); return { height:list.getBoundingClientRect().height, mode:getComputedStyle(list).display, count:list.children.length, expanded:document.getElementById('dashboard-widget-expand').getAttribute('aria-expanded'), prevHidden:document.getElementById('dashboard-widget-scroll-prev').hidden } }''')
print(width,'EXPANDED',expanded)
assert expanded['mode']=='grid'
assert expanded['expanded']=='true' and expanded['prevHidden']
assert expanded['height']>metrics['listHeight']*3
page.locator('#dashboard-widget-expand').click()
assert page.locator('#dashboard-widget-expand').get_attribute('aria-expanded')=='false'
outfile=str(root.parent / f'dashboard_appearance_0326_preview_{width}.png')
page.locator('.appearance-section').screenshot(path=outfile)
print('screenshot',outfile)
page.close()
browser.close()
@@ -0,0 +1,36 @@
// Appearance ribbon: compact by default, with full-width expansion and the same
// persisted visibility/reorder controls. No browser or Rust service required.
import assert from 'node:assert/strict';
import fs from 'node:fs';
const html = fs.readFileSync('src/admin/web/index.html', 'utf8');
const css = fs.readFileSync('src/admin/web/style.css', 'utf8');
const js = fs.readFileSync('src/admin/web/app/preferences.js', 'utf8');
const registry = JSON.parse(fs.readFileSync('src/admin/web/dashboard-widgets.json', 'utf8'));
const locales = ['en_US', 'pl_pl'].map(id => JSON.parse(fs.readFileSync(`langs/${id}.json`, 'utf8')).translations);
for (const id of ['dashboard-widget-list', 'dashboard-widget-expand', 'dashboard-widget-scroll-prev',
'dashboard-widget-scroll-next', 'overview-layout-open-settings', 'ui-dashboard-density']) {
assert(html.includes(`id="${id}"`), `missing ${id}`);
}
assert.match(html, /class="appearance-settings-grid dashboard-appearance-grid"/);
assert.match(css, /\.appearance-settings-grid\.dashboard-appearance-grid\s*\{[^}]*grid-template-columns:\s*minmax\(0, 1fr\)/s,
'dashboard setting layout must use the full available width');
assert.match(css, /\.dashboard-appearance-grid \.dashboard-widget-list\s*\{[^}]*flex-wrap:\s*nowrap;[^}]*overflow-x:\s*auto;/s,
'the default card selector must be a horizontal ribbon, not a tall two-column list');
assert.match(css, /\.dashboard-appearance-grid \.dashboard-widget-list\.is-expanded\s*\{[^}]*display:\s*grid;/s);
assert.match(css, /\.dashboard-widget-settings-controls > \.dashboard-widget-scroll-button/);
assert(js.includes('list.scrollLeft = previousScroll'), 'rebuilding settings must preserve horizontal scroll position');
assert(js.includes('focusedId') && js.includes('preventScroll: true'), 'keyboard focus must survive updates');
assert(js.includes('saveAppearancePreference({ dashboard_hidden_widgets:') &&
js.includes('saveAppearancePreference({ dashboard_widgets:'), 'both APIs must retain persistence');
assert(js.includes('const up = el("button", "←"') && js.includes('const down = el("button", "→"'),
'horizontal order controls must align with the ribbon');
assert.equal(registry.length, 62, 'all configured Overview cards must remain available');
for (const translation of locales) {
for (const key of ['dashboardWidgetsShowAll', 'dashboardWidgetsCompact', 'dashboardWidgetsScrollBack',
'dashboardWidgetsScrollNext', 'dashboardWidgetsScrollHelp',
'dashboardWidgetsMoveEarlier', 'dashboardWidgetsMoveLater']) {
assert(translation[key], `missing ${key}`);
}
}
console.log('PASS: full-width compact Appearance ribbon with expandable 62-card layout, keyboard focus and preference persistence');
@@ -100,8 +100,10 @@ assert(ids.every(id => cards.get(id).hidden), 'hiding cards must still work');
assert(!cards.get('requests').hidden, 'legacy widgets must retain their visibility');
// Native title tooltips do not participate in layout or create horizontal scroll.
assert.doesNotMatch(css, /dashboard-widget-(?:description|setting-label-block)/, 'obsolete custom tooltip styles must be removed');
assert.match(css, /grid-template-columns: repeat\(auto-fit, minmax\(min\(260px, 100%\), 1fr\)\)/,
'effective Appearance card columns must preserve a readable name and right-aligned arrows');
assert.match(css, /\.dashboard-appearance-grid \.dashboard-widget-list\s*\{[^}]*overflow-x: auto;/s,
'Appearance must use a compact horizontal ribbon instead of long half-width columns');
assert.match(css, /\.dashboard-appearance-grid \.dashboard-widget-list > \.dashboard-widget-setting\s*\{[^}]*min-width: 226px;/s,
'Appearance ribbon must retain readable labels and right-aligned reorder buttons');
assert.doesNotMatch(prefsSource, /role", "tooltip"|aria-describedby|dashboard-widget-description/,
'old popover DOM and associations must be removed');
const widgetList = { children: [], replaceChildren(...children) { this.children = children; } };