Fix responsive DSP settings controls

This commit is contained in:
Jonathan Sykes
2026-10-10 22:50:02 +08:00
parent 0c387a01cd
commit f0daeaedd2
33 changed files with 225 additions and 8 deletions

84
perf/settings-layout.mjs Normal file
View File

@@ -0,0 +1,84 @@
#!/usr/bin/env node
// Settings layout regression checks across responsive widths, layouts and themes.
import assert from 'node:assert/strict';
import { createServer } from 'node:http';
import { readFile, mkdir } from 'node:fs/promises';
import { resolve, extname } from 'node:path';
import { chromium, webkit } from 'playwright';
const root = resolve('frontend'), shots = resolve('plans/settings-shots');
await mkdir(shots, { recursive: true });
const mime = { '.html':'text/html', '.js':'text/javascript', '.mjs':'text/javascript', '.css':'text/css', '.json':'application/json', '.woff2':'font/woff2', '.svg':'image/svg+xml', '.png':'image/png' };
const server = createServer(async (req, res) => {
const url = new URL(req.url, 'http://localhost');
const path = resolve(root, '.' + (url.pathname === '/' ? '/index.html' : url.pathname));
if (!path.startsWith(root + '/')) { res.writeHead(403).end(); return; }
try { const body = await readFile(path); res.writeHead(200, { 'Content-Type':mime[extname(path)] || 'application/octet-stream' }).end(body); }
catch { res.writeHead(404).end(); }
});
await new Promise(r => server.listen(0, '127.0.0.1', r));
const origin = `http://127.0.0.1:${server.address().port}`;
const layouts = ['classic','f7-swipe','glass-stage','bento-hub'], themes = ['dark','light','contrast'], widths = [390,768,1280,1440,1920];
const failures = [], results = [];
try {
for (const [engineName, engine] of Object.entries({ chromium, webkit })) {
const browser = await engine.launch();
try {
const context = await browser.newContext({ viewport:{width:1280,height:1000}, serviceWorkers:'block' });
await context.route('**/*', route => route.request().url().startsWith(origin) ? route.continue() : route.abort());
await context.addInitScript(() => localStorage.setItem('_ytpdata', JSON.stringify({
playlists:[], history:[], queue:[], settings:{layout:'glass-stage',theme:'dark',quality:'auto',jamesDsp:{enabled:true,downloadForOffline:false,preset:'warm',params:{'convolver.enabled':1,'spatial.enabled':1},irRef:{name:'Small room'}}}
})));
const page = await context.newPage();
page.on('pageerror', error => failures.push(`${engineName}: pageerror ${error.message}`));
await page.goto(origin);
await page.waitForFunction(() => typeof data !== 'undefined' && typeof Lazy !== 'undefined');
await page.waitForFunction(() => performance.getEntriesByName('ytp:boot').length > 0);
for (const css of ['theme-classic.css','theme-f7.css','theme-glass.css','theme-glass-panel.css','theme-glass-controls.css','theme-bento-hub.css']) await page.addStyleTag({ path:resolve(root,css) });
for (const layout of layouts) {
for (const theme of themes) for (const width of widths) {
await page.setViewportSize({ width, height:1000 });
await page.evaluate(({theme,layout}) => { data.settings.theme=theme; data.settings.layout=layout; data.settings.jamesDsp.enabled=true; document.documentElement.dataset.theme=theme; document.documentElement.dataset.layout=layout; if(typeof EQ!=='undefined')EQ.listDspIRs=async()=>[]; view={type:'settings'}; render(); }, {theme,layout});
await page.waitForSelector('.settings-browser .settings-section-row[data-section="playback"]',{state:'attached'});
await page.evaluate(()=>{const host=document.querySelector('.settings-browser');host.dataset.detail='1';document.querySelectorAll('.settings-section').forEach(panel=>panel.hidden=panel.id!=='settings-section-playback');document.getElementById('jamesDspControls').hidden=false;});
await page.waitForSelector('#setDspSpatialMode',{state:'attached'});
const measured = await page.evaluate(() => {
const rect = e => { const r=e.getBoundingClientRect(); return {x:r.x,y:r.y,width:r.width,height:r.height,right:r.right}; };
const selectors=['.settings-browser','.settings-grid','.settings-detail','#listPane .cards','#jamesDspSettings','.dsp-subsection'];
const boxes=Object.fromEntries(selectors.map(s=>{const e=document.querySelector(s);return [s,e?{clientWidth:e.clientWidth,scrollWidth:e.scrollWidth,rect:rect(e)}:null];}));
const rows=[...document.querySelectorAll('#jamesDspSettings .set-row')].map(row=>{
const label=row.querySelector(':scope > span:first-child'),control=row.querySelector(':scope > input,:scope > select,:scope > .set-control-wrap,:scope > .dsp-file-control');
return {text:row.textContent.trim().slice(0,90),label:label?rect(label):null,control:control?{...rect(control),scrollWidth:control.scrollWidth,clientWidth:control.clientWidth,overflow:getComputedStyle(control).overflow,select:control.querySelector('select')?{...rect(control.querySelector('select')),scrollWidth:control.querySelector('select').scrollWidth,clientWidth:control.querySelector('select').clientWidth}:null}:null,row:rect(row)};
});
const file=document.querySelector('#dspIrFile'),checkbox=document.querySelector('#setDspConvolver'),buttons=[...document.querySelectorAll('.dsp-subsection > button')];
const selects=[...document.querySelectorAll('.settings-browser select')].map(s=>({id:s.id,appearance:getComputedStyle(s).appearance,backgroundImage:getComputedStyle(s).backgroundImage,backgroundColor:getComputedStyle(s).backgroundColor}));
const overflows=[...document.querySelectorAll('#jamesDspSettings *')].filter(e=>e.scrollWidth>e.clientWidth+1).map(e=>({tag:e.tagName,id:e.id,cls:e.className,text:e.textContent.trim().slice(0,100),scroll:e.scrollWidth,client:e.clientWidth,rect:rect(e)}));
return {boxes,rows,file:{clipPath:getComputedStyle(file).clipPath,rect:rect(file),parent:rect(file.parentElement)},checkbox:{appearance:getComputedStyle(checkbox).appearance,width:rect(checkbox).width},selects,buttons:buttons.map(b=>({whiteSpace:getComputedStyle(b).whiteSpace,scrollWidth:b.scrollWidth,clientWidth:b.clientWidth,rect:rect(b)})),overflows,page:{scrollWidth:document.documentElement.scrollWidth,clientWidth:document.documentElement.clientWidth}};
});
const key=`${engineName}/${layout}/${theme}/${width}`;
for (const [selector,box] of Object.entries(measured.boxes)) if(box) {
if(box.scrollWidth>box.clientWidth+1) failures.push(`${key}: ${selector} horizontal overflow ${box.scrollWidth}/${box.clientWidth}`);
}
if(measured.page.scrollWidth>measured.page.clientWidth+1) failures.push(`${key}: document horizontal overflow ${measured.page.scrollWidth}/${measured.page.clientWidth}`);
for(const row of measured.rows) if(row.label&&row.control&&row.control.x<row.label.x+Math.min(row.label.width,80)) failures.push(`${key}: control overlaps/collapses into label`);
if(measured.checkbox.appearance!=='none'||measured.checkbox.width<40) failures.push(`${key}: DSP checkbox is not a themed switch`);
for(const select of measured.selects) if(select.appearance!=='none'||select.backgroundImage==='none'||select.backgroundColor==='rgba(0, 0, 0, 0)') failures.push(`${key}: select #${select.id} is not themed`);
if(measured.file.rect.width>1||measured.file.clipPath==='none') failures.push(`${key}: native file picker is visible`);
for(const b of measured.buttons) if(b.whiteSpace!=='nowrap'||b.rect.height>50) failures.push(`${key}: DSP action button wraps vertically`);
results.push({engine:engineName,layout,theme,width,...measured});
if(width===1280) await page.screenshot({path:resolve(shots,`${engineName}-${layout}-${theme}.png`),fullPage:true});
if(width===1280&&layout==='glass-stage'&&theme==='dark') {
await page.locator('#dspConvolverTitle').scrollIntoViewIfNeeded();
await page.screenshot({path:resolve(shots,`${engineName}-glass-stage-dark-dsp.png`),fullPage:true});
await page.locator('#dspSpatialTitle').scrollIntoViewIfNeeded();
await page.screenshot({path:resolve(shots,`${engineName}-glass-stage-dark-spatial.png`),fullPage:true});
}
}
}
await context.close();
} finally { await browser.close(); }
}
} finally { await new Promise(r=>server.close(r)); }
await (await import('node:fs/promises')).writeFile(resolve(shots,'measurements.json'),JSON.stringify({cases:results.length,failures,screenshots:results.filter(r=>r.width===1280).map(r=>`${r.engine}-${r.layout}-${r.theme}.png`)},null,2));
console.log(`Settings layout: ${results.length} cases, ${failures.length} failures; screenshots in plans/settings-shots/`);
if(failures.length) { console.error(failures.slice(0,30).join('\n')); process.exitCode=1; }