Files
ytplayer/tests/glass-controls.spec.js

88 lines
7.5 KiB
JavaScript

const {test,expect}=require('@playwright/test');
const {differentPixels}=require('./helpers/png-diff');
const {openClassic,showPlayingFixture}=require('./helpers/classic-fixture');
const capture=process.env.YTP_GLASS_CAPTURE;
async function fixture(page,layout,state){
await openClassic(page,{populated:true,settings:{autoPreload:false}});
await page.addStyleTag({content:'*,*::before,*::after{animation:none!important;transition:none!important;caret-color:transparent!important}'});
await page.evaluate(({layout,state})=>{
data.settings.layout=layout;applyAppearance();SaveQueue.resumeAll=async()=>{};
API.cacheList=async()=>({ok:true,items:data.playlists[0].videos.slice(0,3).map((v,i)=>({id:v.id,size:104857600*(i+1)})),total:629145600});
if(state==='downloads'){
const videos=data.playlists[0].videos.slice(0,4);downloading.clear();downloadMeta.clear();cachedIds.clear();
localStorage.setItem('ytpSaveQueue',JSON.stringify(Object.fromEntries(videos.map((v,i)=>[v.id,{...v,at:Date.now(),status:['active','queued','paused','failed'][i],error:i===3?'Connection lost':''}]))));
videos.slice(0,2).forEach((v,i)=>{downloading.add(v.id);downloadMeta.set(v.id,v);SaveQueue.progress(v.id,i?{phase:'waiting'}:{received:10485760,total:104857600});});
}
if(state==='search'){searchResults=data.playlists[0].videos;searchAttempted=true;}
view=state==='playlist'?{type:state,id:'studio'}:{type:state};render();
},{layout,state});
if(state==='playing'){await showPlayingFixture(page);await page.evaluate(()=>{data.settings.layout='glass-stage';applyAppearance();});}
await page.evaluate(async()=>{await document.fonts.ready;await new Promise(resolve=>requestAnimationFrame(()=>requestAnimationFrame(resolve)));});
await page.locator('#listPane').scrollIntoViewIfNeeded();
if(state==='saved')await expect(page.locator('.saved-card')).toHaveCount(3);
if(state==='settings')await expect(page.locator('.settings-section-row').first()).toBeVisible();
}
for(const layout of ['classic','glass-stage'])for(const width of [390,1440])for(const state of ['search','queue','history','playlist','saved','downloads','settings'])test(`${layout} ${state} ${width}`,async({page})=>{
await page.setViewportSize({width,height:900});
let baseline, baselineStyles;
const styles = target => target.locator('.nav-item,.bottom-nav-btn,.card-menu,.card-del,.saved-file-action,.dl-act,.settings-section-row').evaluateAll(nodes=>nodes.map(el=>{const s=getComputedStyle(el),r=el.getBoundingClientRect();return [el.className,r.x,r.y,r.width,r.height,s.color,s.backgroundColor,s.borderRadius,s.fontSize,s.display];}));
if(!capture&&layout==='classic'){
const prior=await page.context().newPage();await prior.setViewportSize({width,height:900});
await prior.route('**/theme-glass-controls.css',r=>r.fulfill({contentType:'text/css',body:''}));
await prior.route('**/glass-controls.js',r=>r.fulfill({contentType:'application/javascript',body:''}));
await fixture(prior,layout,state);baselineStyles=await styles(prior);baseline=await prior.screenshot({path:`/tmp/glass-polish-before-${layout}-${width}-${state}.png`});await prior.close();
}
await fixture(page,layout,state);
const path=`/tmp/glass-polish-${capture||'after'}-${layout}-${width}-${state}.png`;
const screenshot=await page.screenshot({path});
if(!capture&&layout==='classic'){expect(await styles(page)).toEqual(baselineStyles);expect(differentPixels(screenshot,baseline)).toBeLessThanOrEqual(16);}
if(!capture&&layout==='glass-stage'){
const svg=page.locator('#bottomNav .classic-nav-icon').first();expect(await svg.evaluate(el=>getComputedStyle(el).display)).not.toBe('none');
const stroke=await svg.evaluate(el=>getComputedStyle(el).strokeWidth);expect(stroke).toBe('1.9px');
const controls=await page.locator('.saved-file-action,.dl-act,.settings-section-row,.card-menu').evaluateAll(nodes=>nodes.filter(n=>n.getBoundingClientRect().width>0).map(n=>{const r=n.getBoundingClientRect();return [r.width,r.height];}));
expect(controls.every(([w,h])=>w>=44&&h>=44)).toBe(true);
}
});
test('Now Playing Glass reference',async({page})=>{
await page.setViewportSize({width:390,height:900});await fixture(page,'glass-stage','playing');await page.locator('#nowPlayingMeta').scrollIntoViewIfNeeded();
await page.screenshot({path:`/tmp/glass-polish-${capture||'after'}-reference.png`});
});
test('Glass navigation, notes and mini-player controls keep labels and round-trip cleanly',async({page})=>{
await page.setViewportSize({width:390,height:900});await openClassic(page,{populated:true,settings:{autoPreload:false}});await showPlayingFixture(page);
await page.addStyleTag({content:'*,*::before,*::after{animation:none!important;transition:none!important;caret-color:transparent!important}'});
await page.evaluate(async()=>{await document.fonts.ready;await new Promise(resolve=>requestAnimationFrame(()=>requestAnimationFrame(resolve)));});
const markup = () => page.locator('#listPane').evaluate(el=>{const copy=el.cloneNode(true);copy.querySelectorAll('[style=""]').forEach(n=>n.removeAttribute('style'));return copy.outerHTML;});
const before=await markup();
const miniClass=await page.locator('#miniBar').getAttribute('class');
await page.evaluate(()=>{data.settings.layout='glass-stage';applyAppearance();});
await expect(page.locator('.card-menu .glass-control-icon').first()).toBeAttached();
// Isolated installed-navigation fixture: the real app shows this bar in
// standalone portrait mode; ordinary Chromium isn't an installed PWA.
await page.addStyleTag({content:'html[data-layout="glass-stage"] #bottomNav{display:flex}html[data-layout="glass-stage"] #miniBar{display:block}'});
await page.locator('#miniBar').evaluate(el=>el.classList.remove('hidden'));
await expect(page.locator('#miniPlayBtn svg')).toBeVisible();
await expect(page.locator('#miniPlayBtn')).toHaveAttribute('aria-label',/Play|Pause/);
await expect(page.locator('#bottomNav [aria-label="Queue"] svg')).toBeVisible();
expect(await page.locator('#bottomNav [aria-label="Downloads"] .bottom-nav-label').evaluate(el=>el.scrollWidth<=el.clientWidth)).toBe(true);
await page.screenshot({path:'/tmp/glass-polish-navigation-notes-mini.png'});
await page.evaluate(()=>{data.settings.layout='classic';applyAppearance();});
await expect(page.locator('.glass-control-icon')).toHaveCount(0);
await page.locator('#miniBar').evaluate((el,className)=>{el.className=className;},miniClass);
expect(await markup()).toBe(before);
});
test('Glass icons preserve button commands and keyboard focus',async({page})=>{
await page.setViewportSize({width:1440,height:900});await fixture(page,'glass-stage','queue');
const action=page.locator('.card-menu').first();await action.focus();
expect(await action.evaluate(el=>getComputedStyle(el).outlineStyle)).toBe('solid');
await action.press('Enter');await expect(page.locator('#modal')).toBeVisible();
});
test('Glass settings final row is reachable and file actions are not covered',async({page})=>{
await page.setViewportSize({width:390,height:900});await fixture(page,'glass-stage','saved');
const covered=await page.locator('.saved-file-action').evaluateAll(nodes=>nodes.some(el=>{const r=el.getBoundingClientRect();if(r.bottom>innerHeight)return false;const target=document.elementFromPoint(r.x+r.width/2,r.y+r.height/2);return !el.contains(target);}));expect(covered).toBe(false);
await page.setViewportSize({width:1440,height:900});await fixture(page,'glass-stage','settings');
const last=page.locator('.settings-section-row').last();await last.scrollIntoViewIfNeeded();await expect(last).toBeInViewport();
await page.screenshot({path:'/tmp/glass-polish-settings-end.png'});
});