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