Verify mobile geometry and themed media icons across browsers
This commit is contained in:
96
perf/ui-icons.mjs
Normal file
96
perf/ui-icons.mjs
Normal file
@@ -0,0 +1,96 @@
|
||||
#!/usr/bin/env node
|
||||
import assert from 'node:assert/strict';
|
||||
import { mkdir, writeFile } from 'node:fs/promises';
|
||||
import { resolve } from 'node:path';
|
||||
import { chromium, webkit } from 'playwright';
|
||||
import { serve, layouts, themes, contextFor, appearance } from './ui-fixture.mjs';
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
const option = (key, fallback) => args.includes(key) ? args[args.indexOf(key)+1] : fallback;
|
||||
const shots = resolve('plans/ui-shots');
|
||||
await mkdir(shots, {recursive:true});
|
||||
const host = await serve(option('--frontend','frontend'));
|
||||
const results = [];
|
||||
try {
|
||||
for (const [name,engine] of Object.entries({chromium,webkit}).filter(([name]) => option('--browser','all') === 'all' || name === option('--browser','all'))) {
|
||||
const browser = await engine.launch();
|
||||
try {
|
||||
const context = await contextFor(browser,host.origin);
|
||||
const page = await context.newPage();
|
||||
const errors = [];
|
||||
page.on('pageerror', e => errors.push(e.message));
|
||||
await page.goto(host.origin);
|
||||
await page.waitForFunction(() => document.activeElement.id === 'searchInput');
|
||||
await page.evaluate(() => {
|
||||
CarMode.configure({input:els.searchInput,search(){},toast(){},state:()=>({paused:true}),action(){}});
|
||||
const videos = ['Saved offline fixture','Downloading fixture','Streaming fixture'].map((title,i) => ({id:'iconcheck0'+i,title,channel:'Icon fixture',thumbnail:'/icons/icon-192.png',duration:240}));
|
||||
data.queue = videos; data.history = videos;
|
||||
cachedIds.add(videos[0].id); downloading.add(videos[1].id);
|
||||
current = {meta:videos[0]}; view = {type:'queue'}; render();
|
||||
els.playerPane.classList.remove('empty','home-over');
|
||||
els.placeholder.classList.add('hidden'); els.controls.classList.remove('hidden');
|
||||
document.querySelector('#nowPlayingMeta').classList.remove('hidden');
|
||||
updateNowPlayingActions();
|
||||
});
|
||||
await page.waitForSelector('.card.cached .saved-badge');
|
||||
// Reuse this live page: layout changes must repaint the same SVG instances.
|
||||
for (const layout of layouts) for (const theme of themes) {
|
||||
await appearance(page,layout,theme);
|
||||
await page.addStyleTag({content:'*,*::before,*::after { animation:none !important; transition:none !important; }'});
|
||||
const measurements = await page.evaluate(() => {
|
||||
const glyph = /[⬇⤓⇩💾📥↓]/u;
|
||||
const bad = [...document.querySelectorAll('button,a,.saved-badge,.empty-icon,.pl-offline-badge,.home-pl-offline')].filter(n => glyph.test(n.textContent)).map(n=>n.outerHTML);
|
||||
const selectors = ['.card.cached .saved-badge','.card.downloading .saved-badge','#saveBtn','#exportBtn','.bottom-nav-btn[data-view="saved"]','.bottom-nav-btn[data-view="downloads"]','.nav-item[data-view="saved"]','.nav-item[data-view="downloads"]'];
|
||||
const icons = selectors.map(selector => {
|
||||
const el=document.querySelector(selector), svg=el.querySelector('svg');
|
||||
const active=[...el.querySelectorAll('svg')].find(n=>getComputedStyle(n).display!=='none') || svg;
|
||||
const style=getComputedStyle(active), parent=getComputedStyle(el);
|
||||
const uses=[...active.querySelectorAll('use')].filter(n=>getComputedStyle(n).display!=='none');
|
||||
return {selector,hasSvg:!!svg,text:active.textContent,stroke:style.stroke,color:style.color,parentColor:parent.color,weight:style.strokeWidth,uses:uses.map(n=>n.getAttribute('href')),resolved:uses.every(n=>!!document.querySelector(n.getAttribute('href')))};
|
||||
});
|
||||
const shapes = [...document.querySelectorAll('#media-download .media-shape')].filter(n=>getComputedStyle(n).display!=='none').map(n=>n.classList[1]);
|
||||
const probe=document.createElement('span');probe.style.color='var(--accent-bright)';document.body.appendChild(probe);
|
||||
const accent=getComputedStyle(probe).color;probe.remove();
|
||||
return {bad,icons,shapes,accent};
|
||||
});
|
||||
assert.deepEqual(measurements.bad,[],`${name}/${layout}/${theme}: glyph icon`);
|
||||
assert.deepEqual(measurements.shapes,[`media-shape-${layout}`],`${layout}: wrong layout drawing`);
|
||||
for (const icon of measurements.icons) {
|
||||
assert(icon.hasSvg && icon.resolved && !icon.text.trim(),`${icon.selector}: invalid SVG`);
|
||||
assert.equal(icon.stroke,icon.color,`${icon.selector}: ignores currentColor`);
|
||||
assert.equal(icon.color,icon.parentColor,`${icon.selector}: ignores control colour`);
|
||||
assert.equal(parseFloat(icon.weight),({'classic':1.8,'f7-swipe':2,'glass-stage':1.9,'bento-hub':1.8})[layout],`${icon.selector}: wrong stroke family`);
|
||||
}
|
||||
if(layout!=='bento-hub') assert.equal(measurements.icons[0].color,measurements.accent,'badge ignores theme token');
|
||||
assert.deepEqual(measurements.icons[0].uses,['#media-offline']);
|
||||
assert.deepEqual(measurements.icons[1].uses,['#media-progress']);
|
||||
const label=`icons-${name}-${layout}-${theme}`;
|
||||
await page.locator('#listPane').screenshot({path:resolve(shots,`${label}-queue.png`)});
|
||||
// Bento Hub shows either the list or player on phones.
|
||||
await page.evaluate(() => document.querySelector('#listPane').classList.add('hidden'));
|
||||
await page.locator('.np-actions').screenshot({path:resolve(shots,`${label}-playing.png`)});
|
||||
// Glass Stage replaces bottom navigation with its floating chrome.
|
||||
const nav = page.locator('.bottom-nav');
|
||||
if (await nav.isVisible()) await nav.screenshot({path:resolve(shots,`${label}-nav.png`)});
|
||||
else await page.screenshot({path:resolve(shots,`${label}-nav-hidden.png`)});
|
||||
// State updates must keep the SVG and reveal the corresponding concept.
|
||||
const states=await page.evaluate(() => {
|
||||
const id=current.meta.id;
|
||||
const visible=()=>{ els.saveBtn.getBoundingClientRect(); return [...els.saveBtn.querySelectorAll('use')].filter(n=>getComputedStyle(n).display!=='none').map(n=>n.getAttribute('href')); };
|
||||
cachedIds.delete(id);updateNowPlayingActions();const normal=visible();
|
||||
downloading.add(id);updateNowPlayingActions();const progress=visible();
|
||||
downloading.delete(id);cachedIds.add(id);updateNowPlayingActions();const done=visible();
|
||||
markCardCacheState('iconcheck02','downloading');markCardCacheState('iconcheck02','cached');
|
||||
return {normal,progress,done};
|
||||
});
|
||||
assert.deepEqual(states,{normal:['#media-download'],progress:['#media-progress'],done:['#media-offline']},`${name}/${layout}/${theme}: save states`);
|
||||
await page.evaluate(() => document.querySelector('#listPane').classList.remove('hidden'));
|
||||
results.push({browser:name,layout,theme,...measurements});
|
||||
}
|
||||
assert.deepEqual(errors,[],'script errors');
|
||||
await context.close();
|
||||
} finally { await browser.close(); }
|
||||
}
|
||||
} finally { await host.close(); }
|
||||
await writeFile(resolve(shots,'icon-measurements.json'),JSON.stringify(results,null,2));
|
||||
console.log(`Media icons: ${results.length} layout/theme cases passed; Queue, Now Playing and navigation screenshots in plans/ui-shots/`);
|
||||
Reference in New Issue
Block a user