Verify transport geometry, playback states and themed icons across both engines
This commit is contained in:
@@ -474,3 +474,13 @@ check Save before playing / Audio-only / Auto-save settings, open a fresh video,
|
||||
add it to a playlist, and inspect Downloads failure details and Saved after a
|
||||
relaunch/offline playback. Verify available storage and whether iOS evicted
|
||||
previous copies. A failing iPhone sequence is needed to continue the bisect.
|
||||
|
||||
Transport controls use the same shared-definition approach as media/save icons.
|
||||
`node perf/ui-icons.mjs` also checks all four layouts × dark/light/contrast in
|
||||
Chromium and WebKit: shared monochrome symbols, accessible labels, visible
|
||||
44px targets, F7 filled glyphs, runtime layout changes, main/stage/mini/service
|
||||
play-pause state swaps, fullscreen and Car mode refreshes, and actual paired
|
||||
remote toggles against a mocked relay. Transport previews and remote screenshots
|
||||
are saved in ignored `plans/transport-shots/`. Run `node perf/ui-geometry.mjs`
|
||||
for the 320–430px search-bar and Saved-row geometry matrix. OS lock-screen buttons
|
||||
remain platform-rendered through the existing MediaSession handlers.
|
||||
|
||||
6065
perf/results/transport-icons-2026-10-09.json
Normal file
6065
perf/results/transport-icons-2026-10-09.json
Normal file
File diff suppressed because it is too large
Load Diff
80
perf/transport-fixture.mjs
Normal file
80
perf/transport-fixture.mjs
Normal file
@@ -0,0 +1,80 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import {resolve} from 'node:path';
|
||||
import {mkdir} from 'node:fs/promises';
|
||||
import {contextFor,appearance} from './ui-fixture.mjs';
|
||||
export async function transportChecks(page,engine,layout,theme) {
|
||||
const shots=resolve('plans/transport-shots');await mkdir(shots,{recursive:true});
|
||||
const result=await page.evaluate(async()=>{
|
||||
const ids=['playBtn','prevBtn','nextBtn','back10Btn','fwd10Btn','stgBack','stgPlay','stgFwd','miniPlayBtn','servicePrevBtn','servicePlayBtn','serviceNextBtn','rvPrev','rvToggle','rvNext'];
|
||||
const legacy=/[▶⏸⏮⏭⏪⏩⏯◀❚]/u;
|
||||
const bad=[...document.querySelectorAll('button,a,.home-pl-play,.brand-mark,.ph-logo,.empty-icon')].filter(n=>legacy.test(n.textContent)).map(n=>n.outerHTML);
|
||||
const inspect=el=>{
|
||||
const svg=el.querySelector('.transport-icon'),style=getComputedStyle(svg),parent=getComputedStyle(el),box=el.getBoundingClientRect();
|
||||
return {id:el.id,kind:svg?.dataset.transportIcon,href:[...svg.querySelectorAll('use')].find(n=>getComputedStyle(n).display!=='none')?.getAttribute('href'),fill:style.fill,stroke:style.stroke,color:style.color,parentColor:parent.color,legacy:!!el.querySelector('.fill,.legacy-nav-icon'),label:el.getAttribute('aria-label'),width:box.width,height:box.height,visible:box.width>0&&box.height>0};
|
||||
};
|
||||
const icons=ids.map(id=>inspect(document.getElementById(id)));
|
||||
const previous=document.querySelector('#prevBtn svg');
|
||||
const previousBox=[...previous.querySelectorAll('use')].find(n=>getComputedStyle(n).display!=='none').getBBox();
|
||||
const previousX=previousBox.x;
|
||||
const shapes=[...document.querySelectorAll('#playBtn .transport-shape')].filter(n=>getComputedStyle(n).display!=='none').map(n=>n.classList[1]);
|
||||
const old=Object.getOwnPropertyDescriptor(Player.master,'paused');let paused=true;
|
||||
Object.defineProperty(Player.master,'paused',{configurable:true,get:()=>paused});
|
||||
const states=[];
|
||||
for(const state of [true,false,true]){paused=state;updatePlayBtn();states.push(['playBtn','stgPlay','miniPlayBtn','servicePlayBtn'].map(id=>inspect(document.getElementById(id))));}
|
||||
// Exercise the real fullscreen HUD and Car mode's own refresh callbacks.
|
||||
const stage=document.querySelector('.player-stage'); const request=stage.requestFullscreen;stage.requestFullscreen=undefined;
|
||||
await playerFullscreen.enter();paused=false;await new Promise(r=>setTimeout(r,300));
|
||||
const hudPlaying=inspect(document.querySelector('.fullscreen-controls button'));
|
||||
paused=true;await new Promise(r=>setTimeout(r,300));const hudPaused=inspect(document.querySelector('.fullscreen-controls button'));playerFullscreen.leave();stage.requestFullscreen=request;
|
||||
CarMode.configure({input:els.searchInput,search(){},toast(){},state:()=>({paused,title:'Transport fixture'}),action(){}});CarMode.open();
|
||||
const car=[...document.querySelectorAll('.car-controls button')].map(inspect);
|
||||
paused=false;await new Promise(r=>setTimeout(r,550));const carPlaying=inspect(document.querySelectorAll('.car-controls button')[1]);CarMode.close();
|
||||
if(old)Object.defineProperty(Player.master,'paused',old);else delete Player.master.paused;updatePlayBtn();
|
||||
// A preview strip uses the very same symbols to make all actions visible together.
|
||||
const strip=document.createElement('section');strip.id='transportPreview';strip.style.cssText='position:fixed;left:8px;right:8px;top:120px;z-index:30000;padding:20px;background:var(--bg-1);color:var(--text);border:1px solid var(--line);border-radius:20px;display:flex;flex-wrap:wrap;gap:12px';
|
||||
for(const kind of ['previous','back10','play','pause','forward10','next']){const b=document.createElement('button');b.className=kind==='play'||kind==='pause'?'ctrl play':'ctrl';b.title=kind;if(kind==='play'||kind==='pause'){const s=getComputedStyle(document.getElementById('playBtn'));for(const prop of ['width','height','borderRadius','background','color','border','boxShadow','backdropFilter'])b.style[prop]=s[prop];}b.innerHTML=TransportIcons.svg(kind);strip.append(b);}document.body.append(strip);
|
||||
return {bad,icons,previousX,shapes,states,hudPlaying,hudPaused,car,carPlaying};
|
||||
});
|
||||
const label=`${engine}/${layout}/${theme}`;
|
||||
assert.deepEqual(result.bad,[],label+': legacy transport glyph');
|
||||
assert.deepEqual(result.shapes,[`transport-shape-${layout}`],label+': wrong drawing');
|
||||
if(result.icons.find(n=>n.id==='prevBtn').visible)assert.equal(result.previousX,layout==='f7-swipe'?4:5,label+': rendered geometry must switch');
|
||||
for(const icon of [...result.icons,...result.car,result.hudPlaying,result.hudPaused,result.carPlaying]){
|
||||
assert(icon.kind&&!icon.legacy&&icon.href===`#transport-${icon.kind}${layout==='classic'?'':'-'+layout}`,label+': nonshared icon');
|
||||
assert(icon.label,label+': missing accessible name');
|
||||
assert.equal(icon.stroke,icon.color,label+': nonmonochrome stroke');
|
||||
if(layout==='f7-swipe'&&['play','pause','previous','next','forward'].includes(icon.kind))assert.equal(icon.fill,icon.color,label+': F7 glyph must be filled '+JSON.stringify(icon));assert.equal(icon.color,icon.parentColor,label+': detached control colour');
|
||||
if(icon.visible)assert(icon.width>=44&&icon.height>=44,`${label}/${icon.id||icon.kind}: target ${icon.width}x${icon.height}`);
|
||||
}
|
||||
for(const [i,icons] of result.states.entries())for(const icon of icons){assert.equal(icon.kind,i===1?'pause':'play');assert.equal(icon.label,i===1?'Pause':'Play');}
|
||||
assert.equal(result.hudPlaying.kind,'pause');assert.equal(result.hudPaused.kind,'play');assert.equal(result.carPlaying.kind,'pause');
|
||||
await page.locator('#transportPreview').screenshot({path:resolve(shots,`${engine}-${layout}-${theme}-symbols.png`)});
|
||||
await page.evaluate(()=>document.getElementById('transportPreview').remove());
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function remoteTransportFixture(browser,origin,engine) {
|
||||
const context=await contextFor(browser,origin);
|
||||
await context.route('**/api/remote/pair',route=>route.fulfill({json:{ok:true,roomId:'fixture',remoteId:'phone',token:'fixture',hostName:'Transport fixture',name:'Phone'}}));
|
||||
await context.routeWebSocket('**/ws/remote**',ws=>{setTimeout(()=>ws.send(JSON.stringify({type:'hello',hostName:'Transport fixture',state:{paused:true,cur:0,dur:240,volume:1,v:{id:'iconcheck00',title:'Transport fixture',channel:'Fixture'}}})),100);});
|
||||
const page=await context.newPage();const errors=[];page.on('pageerror',error=>errors.push(error.message));
|
||||
await page.goto(origin+'/?pair=123456');await page.waitForSelector('#rvControl:not(.hidden)');
|
||||
await page.waitForFunction(()=>document.getElementById('rvTitle').textContent==='Transport fixture');
|
||||
return {
|
||||
async check(layout,theme){
|
||||
await appearance(page,layout,theme);
|
||||
const states=[];
|
||||
for(let i=0;i<3;i++){
|
||||
states.push(await page.evaluate(()=>['rvPrev','rvToggle','rvNext'].map(id=>{const b=document.getElementById(id),svg=b.querySelector('.transport-icon'),r=b.getBoundingClientRect();return {kind:svg.dataset.transportIcon,href:[...svg.querySelectorAll('use')].find(n=>getComputedStyle(n).display!=='none').getAttribute('href'),label:b.getAttribute('aria-label'),width:r.width,height:r.height};})));
|
||||
if(i<2)await page.locator('#rvToggle').click();
|
||||
}
|
||||
for(const [i,icons] of states.entries()){
|
||||
assert.equal(icons[1].kind,i===1?'pause':'play');assert.equal(icons[1].label,i===1?'Pause':'Play');
|
||||
for(const icon of icons){assert.equal(icon.href,'#transport-'+icon.kind+(layout==='classic'?'':'-'+layout));assert(icon.width>=44&&icon.height>=44);}
|
||||
}
|
||||
await page.locator('.rv-transport').screenshot({path:resolve('plans/transport-shots',`${engine}-${layout}-${theme}-remote.png`)});
|
||||
assert.deepEqual(errors,[],engine+': remote script errors');
|
||||
return states;
|
||||
},close:()=>context.close()
|
||||
};
|
||||
}
|
||||
@@ -4,6 +4,7 @@ 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';
|
||||
import {transportChecks,remoteTransportFixture} from './transport-fixture.mjs';
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
const option = (key, fallback) => args.includes(key) ? args[args.indexOf(key)+1] : fallback;
|
||||
@@ -17,6 +18,7 @@ try {
|
||||
try {
|
||||
const context = await contextFor(browser,host.origin);
|
||||
const page = await context.newPage();
|
||||
const remote=await remoteTransportFixture(browser,host.origin,name);
|
||||
const errors = [];
|
||||
page.on('pageerror', e => errors.push(e.message));
|
||||
await page.goto(host.origin);
|
||||
@@ -85,9 +87,12 @@ try {
|
||||
});
|
||||
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});
|
||||
const transport=await transportChecks(page,name,layout,theme);
|
||||
const remoteStates=await remote.check(layout,theme);
|
||||
results.push({browser:name,layout,theme,...measurements,transport,remoteStates});
|
||||
}
|
||||
assert.deepEqual(errors,[],'script errors');
|
||||
await remote.close();
|
||||
await context.close();
|
||||
} finally { await browser.close(); }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user