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() }; }